Skip to content

Logging ​

Since essence v0.9.0, all TBE packages log through a single PSR-3 helper, tbeLog(), with a shared level policy and automatic webhook context. This page defines the convention every package (and host app) follows.

Level Policy ​

LevelMeaningExamples
infoBusiness event — something that is supposed to happen, happenedinvoice paid, wallet credited, service provisioned, campaign completed
warningExpected user friction or misconfiguration — no stack trace neededvalidation failure, stale callback button, gateway rejected a request, unregistered state answer
errorGenuine bug — always with ['exception' => $e]unhandled exception, failed Telegram API init, failed webhook setup
debugHigh-volume diagnostics, off in productionper-message campaign send failures, backup-restore file progress

With production at LOG_LEVEL=info, the error count becomes a real alarm signal: errors are bugs, warnings are friction, info is the business ledger.

tbeLog() ​

php
tbeLog('billing')->info('Invoice paid', [
    'invoice_id' => $invoice->getKey(),
    'price' => $invoice->price,
]);

tbeLog('essence')->error('Failed to create bot: ' . $e->getMessage(), ['exception' => $e]);

The helper returns a TbeLogger (PSR-3 LoggerInterface). Every entry automatically gains:

  • package — the tag you pass to tbeLog() (omit for none; each package uses its own name: essence, billing, user-wallet, gateway-card, gateway-zibal, affiliates, announcements, settings, user-management, host apps use their app name, e.g. vshop)
  • bot_id, user_id (Telegram peer id), username, chat_id, update_id, update_type, state — pulled from the current wHook() context when a webhook is active; silently omitted otherwise

Subject and placeholders ​

Since essence v0.15.0, a log line should say whose it is and what happened without being expanded. Two things make that the default:

  • Subject prefix. The message is prefixed with bot#<bot_id> user#<user_id> @<username> | , from whichever of those keys the entry has.
  • Placeholders. {key} in the message is filled from the context, PSR-3 style, by the logger itself (the host channel's replace_placeholders doesn't matter). Unknown keys and non-scalar values are left as-is.
php
tbeLog('billing')->info('Invoice #{invoice_id} paid: {price}', [
    'invoice_id' => $invoice->getKey(),
    'price' => $invoice->price,
]);
// bot#3 user#12345 @alice | Invoice #12 paid: 250000  {"invoice_id":12,"price":250000,...}

Keep the message short and human (the ids and the outcome); everything else goes in the context for the log viewer to expand. Domain ids are <model>_id (invoice_id, service_id, order_id).

Outside a webhook: for() ​

A queued job, the scheduler or a gateway callback has no webhook user, so there is nothing to build the subject from. Bind the entry to the user it is about:

php
tbeLog('quota')->for($service->botUser)->info('Quota {status}: service #{service_id} ...', [...]);

for() takes the bot user's bot_id, user_id and username, overriding the webhook's. Anything that isn't a BotUser (a missing relation) binds nothing, since logging never throws.

Audit entries: audit() ​

What an admin or the owner did (role and suspension changes, settings, accepting or rejecting a manual payment) is an audit entry:

php
tbeLog('user-management')->audit('Changed role of user#6001 @alice: member -> admin', [...]);

It is written at info, flagged audit: true, to logging.audit_channel, or to the package's channel when that is unset. The subject is the webhook user, i.e. the admin who acted; name the target in the message.

Logging never throws: any failure inside the logger is swallowed so it can never break bot flow.

Rules ​

  1. One entry per event. Never split one failure across multiple log calls.
  2. Identifiers, not payloads. Log invoice_id, chat_id, amounts — never full update JSON or message text. The full update already goes to the bug-report Telegram chat as a document.
  3. Traces only for bugs. Expected exceptions log at warning with no trace; the catch-all path logs error with ['exception' => $e] (Laravel's formatter appends the stack trace).
  4. Sensitive values are redacted. e.g. settings logs [redacted] for SENSITIVE-type setting values.

Configuration ​

php
// config/tbe-essence.php
'logging' => [
    // Log channel for all TBE packages; null uses the app's default channel.
    'channel' => env('TBE_LOG_CHANNEL'),
    // Per-package overrides, keyed by the tbeLog() tag.
    'channels' => [
        'billing' => 'payments',
        'gateway-card' => 'payments',
    ],
    // Where tbeLog()->audit() entries go; null uses the package's channel.
    'audit_channel' => env('TBE_LOG_AUDIT_CHANNEL'),
    // Also push debug/bug reports to the bug_report Telegram chat.
    'telegram_notify' => env('TBE_LOG_TELEGRAM_NOTIFY', true),
],
  • channel: null (default) — everything goes to the app's default channel; existing bots need no changes.
  • channels — send one package's entries somewhere else, e.g. all money movement to a payments file kept longer than bot traffic.
  • audit_channel — the admin audit trail.
  • telegram_notify — controls whether debugMessage(), mixedDebugMessage() and exceptionReport() also send to the bug_report.telegram_chat_id chat.

Host App Setup (Reference: vshop) ​

vshop separates bot traffic from app/framework logs with two rotated daily channels:

php
// config/logging.php
'telegram' => [
    'driver' => 'daily',
    'path' => storage_path('logs/telegram.log'),
    'level' => env('LOG_LEVEL', 'debug'),
    'days' => env('LOG_DAILY_DAYS', 14),
    'replace_placeholders' => true,
],
dotenv
LOG_CHANNEL=stack
LOG_STACK=daily          # app log, rotated, 14 days
LOG_LEVEL=info           # debug only while actively diagnosing
TBE_LOG_CHANNEL=telegram # bot traffic → telegram-YYYY-MM-DD.log

Both files show up in Log Viewer automatically; filter bot entries by the package context key.

Admin Alerts ​

Since essence v0.15.0. Some problems only the bot's owner can fix: a setting that stopped working, a permission the bot lost, a payment gateway refusing the merchant id. A log line nobody reads doesn't fix them, so tell the owner and admins directly:

php
adminAlert(
    'settings.channel_lock.check_failed',                  // what it is about
    fn () => __('tbe-settings::...alerts.check_failed', [...]), // rendered in the bot's locale
    $bot,       // optional: the webhook's bot when omitted
    $throttle,  // optional: seconds, default admin_alerts.throttle (6h)
);
  • Goes to the bot owner plus every admin (power >= ADMIN) who is reachable and not suspended, once each, prefixed with ⚠️.
  • Throttled per bot and key: a condition detected on every update reaches each admin once per window. Put an id in the key (shop.provisioning_failed.{order}) when every occurrence matters.
  • Works outside a webhook (jobs, the scheduler, gateway callbacks) by building the bot's own API client.
  • Logs one warning per alert sent, with the text; a delivery failure to one admin doesn't stop the others.
  • Not for bugs: those still go to the developer through exceptionReport().
php
// config/tbe-essence.php
'admin_alerts' => [
    'throttle' => env('TBE_ADMIN_ALERT_THROTTLE', 21600),
],

Bug Reports via Telegram ​

exceptionReport($e) (used by the catch-all in ExceptionHandler) does two things:

  1. Logs one error entry with the exception.
  2. If bug_report.telegram_chat_id is set and telegram_notify is on, sends a summary message plus .trace and .update documents to that chat.

debugMessage() / mixedDebugMessage() log a single warning and (optionally) mirror it to the same chat.