Lightweight OpenTelemetry helpers untuk Cloudflare Workers runtime.
Library ini menyediakan:
@opentelemetry/sdk-trace-base).npmrc di root project:@grand-board:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<YOUR_TOKEN>
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
traceparent dari request headers_traceparent dari message bodyimport { 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:
Karena menggunakan @opentelemetry/api interfaces, nanti bisa:
fetch() + waitUntil()Tetap enable [observability.traces] di wrangler.toml:
[observability.traces]
enabled = true
head_sampling_rate = 1
Native traces dan library ini complementary:
| 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