Skip to main content

Observability

gotd can report what it does through three channels:

  • Metrics via OpenTelemetry. Off unless you pass a MeterProvider.
  • Logs via gotd/log. Off unless you pass a Logger.
  • Hooks, which are callbacks for events your application should react to.

Nothing is recorded by default. gotd never falls back to the global OpenTelemetry providers, and with no provider set, instrumentation costs no allocations on the request path.

Setup​

Pass the same providers to the client, the updates manager and the downloader:

import (
"github.com/gotd/log"
"github.com/gotd/log/logzap"
"go.opentelemetry.io/otel"

"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/downloader"
"github.com/gotd/td/telegram/updates"
)

mp := otel.GetMeterProvider() // or your SDK MeterProvider
tp := otel.GetTracerProvider()
lg := logzap.New(zapLogger)

gaps := updates.New(updates.Config{
Handler: dispatcher,
Logger: log.Named(lg, "updates"),
MeterProvider: mp,
TracerProvider: tp,
})

client := telegram.NewClient(appID, appHash, telegram.Options{
UpdateHandler: gaps,
Logger: lg,
MeterProvider: mp,
TracerProvider: tp,
})

// client.Downloader() and client.Download* use the client's MeterProvider and Logger.
// The updates manager needs its own MeterProvider (above). For your own Downloader:
d := downloader.NewDownloader().
WithMeterProvider(mp).
WithLogger(log.Named(lg, "downloader"))

Most events are logged at debug level, so with an info-level logger you only see the abnormal ones (see Logs). Raise the level when you need to see more.

Metric attributes never contain user, chat or channel IDs, file locations or message content, so the number of series stays bounded. IDs only appear in logs.

Several clients in one process​

gotd doesn't label metrics by client. Two clients (e.g. two accounts) sharing one MeterProvider add up into the same counters, and their gauges such as gotd.client.pools overwrite each other. Give each client its own provider that adds an instrumentation-scope attribute to every meter gotd creates:

import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

// scoped adds attrs to the instrumentation scope of every meter.
type scoped struct {
metric.MeterProvider
attrs []attribute.KeyValue
}

func (p scoped) Meter(name string, opts ...metric.MeterOption) metric.Meter {
return p.MeterProvider.Meter(name, append(opts, metric.WithInstrumentationAttributes(p.attrs...))...)
}

mp := scoped{MeterProvider: shared, attrs: []attribute.KeyValue{attribute.String("account", name)}}
// Use mp in telegram.Options and updates.Config of that account.

client.Downloader() uses the client's provider, so its metrics are separated too. A Downloader you share between clients is not.

With Prometheus' OTLP receiver, set otlp.promote_scope_metadata: true to turn the scope attribute into a label (otel_scope_account above).

Metrics​

Client (telegram.Options.MeterProvider)​

MetricTypeAttributesMeaning
gotd.client.connection.statecounterdc, state (connecting, ready, disconnected)Primary connection state changes.
gotd.client.connection.deadcounterdc, kind (primary, data, cdn)Connections found dead (pings, read errors).
gotd.client.connection.reconnectscounterdcReconnects of the primary connection.
gotd.client.connection.restartscounterreason (timeout, migration)Restarts requested by the client itself; each is also counted in connection.reconnects.
gotd.client.migrationscounterdc (target)Home DC changes (*_MIGRATE).
gotd.client.auth.lostcounterreason, kindAuthorized → lost transitions (see OnAuthLost). reason is the error type, kind is banned, deleted, revoked, unregistered, expired or key_invalid.
gotd.client.account.restrictedcounterreasonErrors telling that the account is restricted: FROZEN_METHOD_INVALID, FROZEN_PARTICIPANT_MISSING, PHONE_NUMBER_BANNED.
gotd.client.request.timeoutscounterresult (resent, failed), methodRequests that missed their RequestTimeout deadline.
gotd.client.pool.createdcounterdc, kind (file, migrate), result (ok, error, canceled)Connection pools created for other DCs.
gotd.client.pool.create.durationhistogram, skind, resultTime to create a pool (connect and authorization transfer).
gotd.client.pool.evictedcounterdc, kind, reasonCached pools dropped, e.g. after AUTH_KEY_UNREGISTERED.
gotd.client.file.migrationscounterdc (target), sourceFILE_MIGRATE hops.
gotd.client.poolsgaugekind (file, migrate, cdn)Open pools while Run runs.
gotd.client.invoke.durationhistogram, sresult (ok, rpc_error with code, canceled, error)Duration of every request, including client.API() calls.

invoke.duration has no method attribute: one series per TL method would be too many. For per-method metrics, add a middleware.

Updates (updates.Config.MeterProvider)​

MetricTypeAttributesMeaning
gotd.updates.difference.requestscounterkind (common, channel), result (empty, slice, full, too_long, error, inaccessible)getDifference / getChannelDifference calls.
gotd.updates.difference.durationhistogram, skind, resultTheir duration.
gotd.updates.difference.triggerscounterkind, reason (startup, pts-gap-timeout, idle-timeout, channel-too-long, …)Why a difference was requested.
gotd.updates.gapscounterkind, seq (pts, qts, seq), action (detected, resolved_by_waiting, resolved_by_pending)Sequence gaps and how they were closed.
gotd.updates.channel.too_longcounterdialog (ok, stale, invalid)channelDifferenceTooLong gaps, one per OnChannelGap call.
gotd.updates.channel.gap.sizehistogram, ptsRemotePts - LocalPts of those gaps.
gotd.updates.channel.gap.messageshistogramNumber of messages returned with a gap.
gotd.updates.before_runcounter, batchesaction (passed, buffered, dropped, replayed, passed_raw)Updates received before Run, see BeforeRun.
gotd.updates.channel.trackcountersource (api, stored, update, too_long), resultChannels starting to be tracked.
gotd.updates.channels.trackedup-down counterChannels tracked now.
gotd.updates.channel.inaccessiblecounterChannels dropped after CHANNEL_PRIVATE.
gotd.updates.stoppedcounteropCalls rejected with updates.ErrStopped.
gotd.updates.dispatchedcountersource (live, difference)Updates passed to the handler.

Downloader (Downloader.WithMeterProvider)​

MetricTypeAttributesMeaning
gotd.downloader.downloadscounterresult (ok, error, canceled)Finished downloads.
gotd.downloader.requestscounterkind (master, cdn, web), result (ok, flood_wait, timeout, canceled, error)Part requests.
gotd.downloader.request.durationhistogram, skindPart request duration.
gotd.downloader.bytescounter, BykindBytes downloaded.
gotd.downloader.resumescounterDownloads started with WithOffset.
gotd.downloader.cdn.redirectscounterFiles served from a CDN DC.
gotd.downloader.cdn.reuploadscounterupload.reuploadCdnFile requests.
gotd.downloader.hash.mismatchescounterkind (cdn, verify)Parts that failed hash verification.
gotd.downloader.file_reference.refreshescounterresult (ok, not_found, error, limit)WithRefresh attempts.
gotd.downloader.verifycounterresult (complete, partial, none, error)Builder.Verify results.
gotd.downloader.retriescounteroperationRetried part requests.
gotd.downloader.retries.exhaustedcounteroperationRequests that ran out of retries.
gotd.downloader.scheduler.backoffscounterreason (flood_wait, timeout)Backoffs reported to a Scheduler.
gotd.downloader.scheduler.windowgaugeyour attributesCurrent window of a Scheduler.
gotd.downloader.scheduler.in_flightgaugeyour attributesPart requests in flight.
gotd.downloader.scheduler.waitinggaugeyour attributesPart requests waiting for a slot.
gotd.downloader.scheduler.decreasescounteryour attributesWindow decreases.

Scheduler gauges are registered by Scheduler.Instrument, with your own attributes, for example the DC. Schedulers the client creates from Options.DownloadScheduler are registered with a dc attribute while Run runs. For your own schedulers:

reg, err := sched.Instrument(meter, attribute.Int("dc", 2))
defer reg.Unregister()

What to watch​

  • Counters versus state. connection.state counts transitions. The first ready after start is a counter's first value, which PromQL's increase() doesn't show, and an idle client looks the same as a dead one. To show disconnects, chart increases of state="disconnected" and connection.dead. "Connected now" can't be read from these counters.
  • The client is healthy:
    • connection.state{state="ready"} grows only on reconnects;
    • connection.dead and request.timeouts stay near zero;
    • invoke.duration p95 is stable.
  • Updates are complete:
    • difference.requests{result="too_long"} and channel.too_long are rare;
    • gaps{action="detected"} is followed by a resolution;
    • before_run{action="dropped"} is zero, unless you chose BeforeRunDrop.
  • Authorization: any increase of auth.lost needs attention; kind="banned" or "deleted" means the account is gone. Any account.restricted means a frozen account or a banned number.
  • Downloads keep up:
    • downloads{result="error"}, hash.mismatches and retries.exhausted stay near zero;
    • scheduler.window recovers after scheduler.backoffs.

Logs​

Events are logged through the Logger you pass. Attributes are structured fields, and IDs (channel, DC, pts) appear here, never in metrics.

LevelComponentEvents
DebugclientConnection state, dead data and CDN connections, pool created, FILE_MIGRATE hop, restart requested
InfoclientHome DC migration (from, to), popup service notifications (type, text)
WarnclientDead primary connection, pool creation failed, pool evicted after AUTH_KEY_UNREGISTERED, lost authorization, request timeout, AUTH_KEY_DROP_* notification, frozen account (since, until, appeal URL)
ErrorclientLost authorization of a banned or deleted account, PHONE_NUMBER_BANNED
DebugupdatesEach difference request with result and duration, channel tracked, update received before Run
InfoupdatesChannel gap (channel, local and remote pts, top message, number of messages), replay of updates buffered before Run
Warnupdatesupdates.differenceTooLong, channel pts that did not advance, failed TrackChannel
DebugdownloaderPart requests, CDN redirect, reupload, file reference refreshed
WarndownloaderHash mismatch, failed refresh, retries exhausted

Hooks​

Use hooks to act on events. Use metrics and logs to watch them.

Client (telegram.Options)​

HookCalled when
OnConnectionStateEvent func(ConnectionStateEvent)The primary connection state changes; DC, State. Prefer it to OnConnectionState.
OnConnDead func(ConnDeadEvent)Any connection is dead; DC, Kind, Err. Prefer it to OnDead.
OnAuthLost func(ctx, err) errorAn authorized session is lost. Runs on its own goroutine with Run's context. Return an error to stop Run with *AuthLostError. See Long-running clients.
OnFrozen func(ctx, FreezeState)A request failed because the account is frozen, and app config confirms it. Checked at most once an hour. See Frozen accounts.
OnSelfError / OnSelfSuccessThe Self call after connecting fails or succeeds.
OnTransferAuthorization is transferred to another DC.
RequestTimeout func(bin.Encoder) time.DurationPer-request response deadline; one resend after a reconnect. Return 0 for requests that must not be sent twice.
PingInterval, PingTimeoutLiveness checks of connections.
MiddlewaresWrap every request, e.g. for per-method metrics or rate limits.

Updates (updates.Config)​

HookCalled when
OnChannelGap func(ChannelGap)A channel gap can't be recovered. Has the pts range, the top message, and the latest Messages with Users and Chats; backfill with messages.getHistory.
OnChannelTooLong func(channelID)Same event, ID only.
OnChannelInaccessible func(channelID)Access to a channel is lost (CHANNEL_PRIVATE); it stops being tracked.
OnTooLong func()The common state gap can't be recovered (updates.differenceTooLong).
OnLoadUserStateFailed func()No stored state on start, so a full resync follows.
OnLoadChannelStateFailed func(channelID)A stored channel can't be loaded; track it again with Manager.TrackChannel.
BeforeRun, BeforeRunBufferSizeWhat happens to updates received before Run: pass, buffer or drop.

Dispatcher (tg.UpdateDispatcher)​

HookCalled when
OnBefore func(ctx, e, update) errorEvery update, before its handler, including updates without one. Return an error to skip the handler. Useful for per-type counters or tracing.
OnFallbackAn update has no handler.
OnAny*One handler for private chats, groups and channels; see Handling updates.

Downloads​

HookCalled when
Builder.WithProgress(func(done, total int64))Bytes are written.
Builder.WithRefresh(RefreshFunc)A file reference expired; return a fresh location (see downloader/fileref).
Scheduler.Snapshot()Read the window, in-flight, waiting and decreases at any time.

Other packages​

HookPackage
Config.OnInitDelaytelegram/takeout: TAKEOUT_INIT_DELAY while waiting for the export to be allowed.
Waiter.WithCallback(func(ctx, FloodWait)), Waiter.Snapshot()gotd/contrib/middleware/floodwait: every flood wait (type, wait, error) and per-type state.

Tracing​

TracerProvider in telegram.Options and updates.Config adds spans for requests and update processing; see Debugging.