Skip to content

gramiojs/opentelemetry

Repository files navigation

@gramio/opentelemetry

npm npm downloads JSR JSR Score

OpenTelemetry tracing plugin for GramIO.

Automatically creates spans for every incoming update and outgoing Telegram API call, giving you full distributed tracing for your Telegram bot.

Installation

npm install @gramio/opentelemetry @opentelemetry/api @opentelemetry/sdk-node

Usage

Important

You must set up the OpenTelemetry SDK before creating the bot.

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Bot } from "gramio";
import { opentelemetryPlugin } from "@gramio/opentelemetry";

const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({
        url: "http://localhost:4318/v1/traces",
    }),
    serviceName: "my-telegram-bot",
});
sdk.start();

const bot = new Bot(process.env.BOT_TOKEN!)
    .extend(opentelemetryPlugin())
    .on("message", async (context) => {
        await context.send("Hello!");
    });

await bot.start();

What it does automatically

Every incoming update creates a root span with Telegram context, and every API call becomes a child span under it:

gramio.update.message (CONSUMER)
  ├── telegram.api/sendMessage (CLIENT)
  ├── telegram.api/deleteMessage (CLIENT)
  └── custom spans via record()

Span attributes

Update spans (gramio.update.{type}):

Attribute Example
gramio.update_type "message"
gramio.user_id 123456789
gramio.chat_id -100123456
gramio.chat_type "private"

API call spans (telegram.api/{method}):

Attribute Example
telegram.api.method "sendMessage"
telegram.api.request.chat_id 123456 (only with recordApiParams: true)
telegram.api.request.text "Hello!" (only with recordApiParams: true)

When recordApiParams is enabled, only primitive values (string, number, boolean) from the top-level params are recorded as flat attributes following OTEL semantic conventions. Complex objects (reply_markup, etc.) are skipped.

Error recording

  • Handler errors — recorded on the update span via .onError() with status ERROR and exception details
  • API errors — recorded as span events with method, error code, and message
  • API call failures — the child span gets ERROR status and the exception is re-thrown

Options

opentelemetryPlugin({
    tracerName: "gramio",         // Custom tracer name (default: "gramio")
    recordApiParams: false,       // Record API params as attributes (default: false)
})

Warning

Enabling recordApiParams may capture sensitive data (tokens, user messages). Use with caution.

Utilities

record(name, fn)

Create a child span for a block of code. Automatically ends the span and records exceptions:

import { record } from "@gramio/opentelemetry";

bot.on("message", async (context) => {
    const users = await record("database.query", async () => {
        return db.query("SELECT * FROM users");
    });

    await context.send(`Found ${users.length} users`);
});

getCurrentSpan()

Get the current active span from anywhere in the async chain:

import { getCurrentSpan } from "@gramio/opentelemetry";

function myUtility() {
    const span = getCurrentSpan();
    span?.addEvent("my.custom.event", { key: "value" });
}

setAttributes(attributes)

Sugar for getCurrentSpan()?.setAttributes(...):

import { setAttributes } from "@gramio/opentelemetry";

function myUtility() {
    setAttributes({ "custom.attribute": "value" });
}

Elysia + GramIO: Shared Tracing via Webhook

When using Elysia as a webhook server with @elysiajs/opentelemetry, both frameworks share the same OpenTelemetry context through AsyncLocalStorage. This means a single trace spans the entire request lifecycle:

HTTP POST /webhook (Elysia root span, SERVER)
  └── gramio.update.message (CONSUMER)
        ├── telegram.api/sendMessage (CLIENT)
        └── database.query (custom via record())

Setup

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Elysia } from "elysia";
import { opentelemetry } from "@elysiajs/opentelemetry";
import { Bot } from "gramio";
import { opentelemetryPlugin } from "@gramio/opentelemetry";

// 1. Initialize OpenTelemetry SDK (once, before everything)
const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({
        url: "http://localhost:4318/v1/traces",
    }),
    serviceName: "my-bot-service",
});
sdk.start();

// 2. Create GramIO bot with OpenTelemetry plugin
const bot = new Bot(process.env.BOT_TOKEN!)
    .extend(opentelemetryPlugin())
    .on("message", async (context) => {
        await context.send("Hello from webhook!");
    });

// 3. Create Elysia server with OpenTelemetry + webhook endpoint
const app = new Elysia()
    .use(opentelemetry())
    .post("/webhook", async ({ body }) => {
        // GramIO processes the update inside Elysia's active span context.
        // The gramio.update.* span automatically becomes a child of the HTTP span.
        await bot.updates.handleUpdate(body);
        return "ok";
    })
    .listen(3000);

// 4. Set webhook URL with Telegram
await bot.api.setWebhook({
    url: "https://your-domain.com/webhook",
});

console.log("Webhook server running on :3000");

Why this works

Both @elysiajs/opentelemetry and @gramio/opentelemetry use @opentelemetry/api which relies on AsyncLocalStorage for context propagation. When Elysia handles the webhook POST:

  1. Elysia creates a root HTTP span and activates it in the async context
  2. The request handler calls bot.updates.handleUpdate(body)
  3. GramIO's .use() middleware calls tracer.startActiveSpan() — which sees the Elysia span as the parent
  4. API calls inside the handler become grandchildren of the HTTP span
  5. The entire trace flows seamlessly across both frameworks

No additional configuration needed — it just works because they share the same global @opentelemetry/api instance.

Export to backends

Jaeger

import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({
        url: "http://localhost:4318/v1/traces",
    }),
    serviceName: "my-telegram-bot",
});

Grafana Cloud / Tempo

const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({
        url: "https://tempo-us-central1.grafana.net/tempo",
        headers: {
            Authorization: `Basic ${btoa(`${instanceId}:${apiKey}`)}`,
        },
    }),
    serviceName: "my-telegram-bot",
});

Axiom

const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({
        url: "https://api.axiom.co/v1/traces",
        headers: {
            Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
            "X-Axiom-Dataset": process.env.AXIOM_DATASET!,
        },
    }),
    serviceName: "my-telegram-bot",
});

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •