@grand-board/otel-cloudflare - v6.11.0
    Preparing search index...

    @grand-board/otel-cloudflare - v6.11.0

    otel-cloudflare

    Lightweight OpenTelemetry helpers untuk Cloudflare Workers runtime.

    Library ini menyediakan:

    • Trace context propagation antar services (fetch → queue → consumer)
    • Structured logging dengan trace ID untuk log correlation
    • Custom TracerProvider yang works di Workers runtime (tanpa dependency ke @opentelemetry/sdk-trace-base)
    1. Tambahkan .npmrc di root project:
    @grand-board:registry=https://npm.pkg.github.com
    //npm.pkg.github.com/:_authToken=<YOUR_TOKEN>
    1. Install package:
    pnpm add @grand-board/otel-cloudflare @opentelemetry/api
    

    Gunakan instrument() untuk auto-setup trace context:

    import { instrument, getLogger, withTraceContext } from "@grand-board/otel-cloudflare";

    export default instrument({
    async fetch(request, env, ctx) {
    const logger = getLogger();
    logger.info("handling request"); // [trace_id] handling request

    // Propagate trace ke queue
    await env.QUEUE.send(withTraceContext({ orderId: 123 }));

    return new Response("OK");
    },

    async queue(batch, env, ctx) {
    const logger = getLogger();
    // Trace ID otomatis di-extract dari message
    logger.info("processing batch"); // [same_trace_id] processing batch

    for (const msg of batch.messages) {
    logger.info("processing message", { id: msg.id });
    msg.ack();
    }
    },

    async scheduled(controller, env, ctx) {
    const logger = getLogger();
    // Scheduled selalu dapat trace ID baru
    logger.info("running cron", { cron: controller.cron }); // [new_trace_id] running cron
    },
    });

    Gunakan withTrace() dengan parent option:

    // hooks.server.ts
    import {
    Logger,
    withTrace,
    getTraceparent,
    initTracing,
    } from "@grand-board/otel-cloudflare";

    // Initialize once
    initTracing();

    export const handle: Handle = async ({ event, resolve }) => {
    const traceparent = event.request.headers.get("traceparent");

    return withTrace(
    async () => {
    const logger = new Logger();
    logger.info("handling request"); // [trace_id] handling request

    // Get current traceparent for propagation
    const currentTrace = getTraceparent();

    // Send to queue with trace context
    await env.QUEUE.send({
    data: payload,
    _traceparent: currentTrace
    });

    return resolve(event);
    },
    { parent: traceparent, name: "handleRequest" }
    );
    };
    ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
    helios-web │ ──► │ Queue │ ──► │ consumer
    │ [abc123] │ │ _traceparent│ │ [abc123] │
    └─────────────┘ └─────────────┘ └─────────────┘

    Query: trace_id = "abc123"Dapat semua logs
    • fetch: Extract traceparent dari request headers
    • queue: Extract _traceparent dari message body
    • scheduled: Generate trace ID baru
    import { Logger, getLogger, runWithLogger, withAttrs } from "@grand-board/otel-cloudflare";

    const logger = new Logger({
    attrs: { service: "my-service", environment: "production" }
    });

    // Basic logging - otomatis include trace_id
    logger.info("user logged in", { userId: 42 });
    // Output: {"level":"info","msg":"[abc123] user logged in","time":"...","userId":42,"trace_id":"abc123"}

    // Child logger
    const requestLogger = logger.child({ requestId: "req-456" });
    requestLogger.info("processing"); // includes requestId in all logs

    // Contextual attributes
    withAttrs({ userId: 42 }, () => {
    logger.info("user action"); // includes userId
    });

    // Logger in context
    runWithLogger(logger, () => {
    const log = getLogger();
    log.info("from context");
    });

    Capture source code location untuk debugging:

    import { CallerInfo, withCaller, getCurrentCaller } from "@grand-board/otel-cloudflare";

    const caller = CallerInfo.from();
    console.log(caller.toString()); // "src/handler.ts:42 handleRequest"
    console.log(caller.toAttributes());
    // { "code.filepath": "src/handler.ts", "code.lineno": 42, "code.function": "handleRequest" }

    Decorator @traceWorkflow() untuk auto-instrument Cloudflare Workflows dengan OpenTelemetry tracing.

    import { traceWorkflow, WorkflowEvent, WorkflowStep } from "@grand-board/otel-cloudflare";
    import { WorkflowEntrypoint } from "cloudflare:workers";

    interface Env {
    MY_QUEUE: Queue;
    }

    interface OrderPayload {
    orderId: string;
    items: string[];
    }

    @traceWorkflow()
    export class OrderWorkflow extends WorkflowEntrypoint<Env, OrderPayload> {
    async run(event: WorkflowEvent<OrderPayload>, step: WorkflowStep) {
    // Setiap step.do() otomatis di-trace sebagai child span
    const validated = await step.do("validate-order", async () => {
    return this.validateOrder(event.payload);
    });

    await step.do("process-payment", async () => {
    return this.processPayment(validated);
    });

    // step.sleep, sleepUntil, waitForEvent juga di-trace
    await step.sleep("wait-for-inventory", "5 minutes");

    await step.do("ship-order", async () => {
    return this.shipOrder(validated);
    });

    return { success: true, orderId: event.payload.orderId };
    }
    }

    Untuk menyambungkan trace dari caller (misalnya fetch handler) ke workflow:

    import { instrument, withWorkflowTrace } from "@grand-board/otel-cloudflare";

    interface Env {
    ORDER_WORKFLOW: Workflow;
    }

    export default instrument<Env>({
    async fetch(request, env, ctx) {
    const payload = await request.json();

    // withWorkflowTrace() inject _traceparent ke payload
    const instance = await env.ORDER_WORKFLOW.create({
    params: withWorkflowTrace({
    orderId: payload.orderId,
    items: payload.items,
    }),
    });

    return Response.json({ instanceId: instance.id });
    },
    });

    Hasilnya, workflow akan menjadi child span dari fetch handler:

    ┌─────────────────────────────────────────────────────────────────┐
    fetch handler [trace_id: abc123] │
    │ └─► workflow:OrderWorkflow [trace_id: abc123] │
    │ ├─► step:validate-order
    │ ├─► step:process-payment
    │ ├─► step:wait-for-inventory:sleep
    │ └─► step:ship-order
    └─────────────────────────────────────────────────────────────────┘

    Untuk logging/debugging, bisa specify custom key dari payload:

    // Shorthand - langsung pass function
    @traceWorkflow((event) => event.payload.orderId)
    export class OrderWorkflow extends WorkflowEntrypoint<Env, OrderPayload> {
    // ...
    }

    // Atau dengan options object
    @traceWorkflow({
    key: (event) => [event.payload.orderId, event.payload.customerId],
    name: "order-processing-workflow", // custom span name
    })
    export class OrderWorkflow extends WorkflowEntrypoint<Env, OrderPayload> {
    // ...
    }

    Retry dan timeout config tetap berjalan normal:

    @traceWorkflow()
    export class MyWorkflow extends WorkflowEntrypoint<Env, Payload> {
    async run(event: WorkflowEvent<Payload>, step: WorkflowStep) {
    // Dengan retry config
    await step.do(
    "fetch-external-api",
    {
    retries: { limit: 3, delay: "1s", backoff: "exponential" },
    timeout: "30s",
    },
    async () => {
    return fetch("https://api.example.com/data");
    }
    );

    // Wait for external event
    const approval = await step.waitForEvent<{ approved: boolean }>(
    "wait-approval",
    { type: "approval-response", timeout: "24 hours" }
    );

    if (!approval.approved) {
    throw new Error("Order rejected");
    }
    }
    }

    Setiap span yang dibuat memiliki attributes:

    Attribute Description
    workflow.name Nama workflow class atau custom name
    workflow.instance_id Cloudflare workflow instance ID
    workflow.key Deterministic key dari payload
    workflow.step.name Nama step
    workflow.step.type Tipe step: sleep, sleepUntil, waitForEvent
    workflow.step.duration Duration untuk sleep
    workflow.step.timestamp Target timestamp untuk sleepUntil
    workflow.step.event_type Event type untuk waitForEvent
    workflow.step.timeout Timeout untuk waitForEvent
    Function Description
    instrument(handler, opts?) Wrap ExportedHandler dengan auto trace context
    withTraceContext(body) Inject _traceparent ke message body untuk queue propagation
    initTracing() Initialize TracerProvider (called automatically by instrument)
    Function Description
    withTrace(fn, opts?) Wrap function dengan span, support parent option
    getTraceparent() Get current trace as W3C traceparent string
    parseTraceparent(str) Parse traceparent string ke TraceContext
    withParentTrace(parent, fn) Run function dengan specific parent context
    Method Description
    logger.trace/debug/info/warn/error/fatal(msg, attrs?, opts?) Log dengan level
    logger.child(attrs) Create child logger dengan additional attributes
    logger.run(fn) Run function dengan logger di context
    Function Description
    getLogger() Get logger dari context (AsyncLocalStorage)
    runWithLogger(logger, fn) Run dengan logger di context
    withAttrs(attrs, fn) Run dengan contextual attributes
    getAttrs() Get current contextual attributes
    Method Description
    CallerInfo.from(skipFrames?) Capture caller dari stack trace
    caller.toAttributes() Return OpenTelemetry attributes
    caller.toString() Format: "file:line function"
    caller.isEmpty() Check if empty
    Function/Decorator Description
    @traceWorkflow(opts?) Decorator untuk auto-trace workflow class
    @traceWorkflow(keyFn) Shorthand dengan key extractor function
    withWorkflowTrace(payload) Inject _traceparent ke workflow payload
    generateDeterministicKey(value) Generate consistent key dari value (untuk debugging)

    TraceWorkflowOptions:

    Option Type Description
    key (event) => unknown Extract key dari event untuk logging
    name string Custom workflow name untuk span

    @opentelemetry/sdk-trace-base tidak compatible dengan Cloudflare Workers karena dependency ke Node.js APIs (perf_hooks, etc). Library ini menyediakan lightweight TracerProvider yang:

    • ✅ Generate valid W3C trace ID & span ID
    • ✅ Maintain OpenTelemetry context (untuk logger integration)
    • ✅ Works di Workers runtime
    • ❌ Tidak export spans (no-op exporter)

    Karena menggunakan @opentelemetry/api interfaces, nanti bisa:

    1. Add OTLP exporter via fetch() + waitUntil()
    2. Atau gunakan Cloudflare Destinations untuk export native traces

    Tetap enable [observability.traces] di wrangler.toml:

    [observability.traces]
    enabled = true
    head_sampling_rate = 1

    Native traces dan library ini complementary:

    • Native traces: Auto-instrument I/O (D1, KV, R2, fetch)
    • This library: Log correlation & trace propagation antar services
    Feature Status
    Log correlation across services ✅ Works
    Trace propagation (fetch → queue → consumer) ✅ Works
    Single trace view di Cloudflare Dashboard ❌ Not supported by Cloudflare
    Match trace ID dengan Cloudflare native traces ❌ No API exposed
    Microsecond precision timing ❌ Workers uses Date.now()

    Apache-2.0