@yaebal/otel
opentelemetry tracing: every update becomes a span, every bot api call made while handling it a child span — so a slow handler, a flood wait or a failing method is one trace away in jaeger, grafana tempo, honeycomb, axiom or any other otel backend.
install
pnpm add @yaebal/otel @opentelemetry/apithe only dependency is @opentelemetry/api — bring your own sdk and exporter, the
same ones the rest of your services use.
usage
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Bot } from "@yaebal/core";
import { otel } from "@yaebal/otel";
// the sdk registers the async context manager that links child spans to their update
new NodeSDK({ serviceName: "my-bot", traceExporter: new OTLPTraceExporter() }).start();
// install first, so the update span wraps every other plugin
const bot = new Bot(process.env.BOT_TOKEN!).install(otel());
bot.command("report", async (ctx) => {
const rows = await ctx.otel.record("db.query", () => db.report()); // a child span
ctx.otel.setAttributes({ "report.rows": rows.length });
await ctx.send(render(rows)); // telegram.api/sendMessage, a child of the update
});
await bot.start();a trace for one update:
telegram.update/message CONSUMER update_id, update_type, chat.id, chat.type, user.id
├── db.query INTERNAL (ctx.otel.record)
└── telegram.api/sendMessage CLIENT method, chat.id, error_code on failurewhat lands on ctx
| field | does |
|---|---|
ctx.otel.span | the update's span |
ctx.otel.record(name, fn) | runs fn in a child span — ended for you, marked failed if it throws |
ctx.otel.setAttributes(attrs) | adds attributes to the update span |
options
bot.install(
otel({
tracer: trace.getTracer("my-bot"), // default: the global provider's "@yaebal/otel"
recordApiParams: true, // params as a json attribute — off by default
attributes: (ctx) => ({ "tenant.id": tenantOf(ctx) }),
}),
);| option | default | does |
|---|---|---|
tracer | trace.getTracer("@yaebal/otel") | the tracer to record with |
recordApiParams | false | attach each call's params as telegram.params — json, uploads named instead of inlined, cut at 4096 chars. off by default because params carry user text |
attributes | — | (ctx) => Attributes added to every update span |
behavior
- a handler that throws marks the update span
ERRORwith the exception recorded, then rethrows tobot.onErroras usual. a failed api call does the same on its own span, plustelegram.error_code; each retry attempt (with@yaebal/again) is its own span. - the bot token never reaches a span — only method names and, opt-in, params.
- without an async context manager (no sdk registered) api spans still record, but as roots instead of children of their update.
without a bot
import { createApi } from "@yaebal/core";
import { instrumentApi } from "@yaebal/otel";
// a client without a bot — e.g. a broadcast worker
const api = instrumentApi(createApi(process.env.BOT_TOKEN!));related
- @yaebal/audit-log — structured logs of every update and api call
- @yaebal/analytics — product events, not traces
- troubleshooting —
YAEBAL_DEBUGfor quick local inspection