Skip to content

Distributed Tracing

A trace ID is a string that links related log entries together. You generate one at the start of an operation and pass it to every log in that operation. When you log with the same traceId, you can see the full journey of a request — from frontend button click, through API route, to database query and back.

const traceId = `checkout-${orderId}-${Date.now()}`;
gunsole.info({
bucket: "api",
message: "Checkout started",
traceId,
tags: { step: "start" },
});
// ... later
gunsole.info({
bucket: "payment",
message: "Payment processed",
traceId,
tags: { step: "payment" },
});
// ... later
gunsole.info({
bucket: "api",
message: "Checkout completed",
traceId,
tags: { step: "complete" },
});

In the desktop app, you can search or filter by trace ID to see the entire flow of an operation across buckets.

Use whatever format makes sense for your app. Some patterns:

// UUID-based
const traceId = crypto.randomUUID();
// Descriptive prefix (from example apps)
const traceId = `trace-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
// Semantic (e.g., for checkout flows)
const traceId = `checkout:order-${orderId}`;

The only requirement is that it’s a string. Make it descriptive enough that you can identify the operation when you see it in the log viewer.

Frontend → generates traceId, passes it to backend via header

Section titled “Frontend → generates traceId, passes it to backend via header”
const traceId = crypto.randomUUID();
gunsole.info({
bucket: "checkout",
message: "Submitting order",
traceId,
context: { items: cart.length },
});
const response = await fetch("/api/orders", {
method: "POST",
headers: { "x-trace-id": traceId },
body: JSON.stringify({ items: cart }),
});
Section titled “Backend → reads traceId from header, uses it in all related logs”
app.post("/api/orders", async (req, res) => {
const traceId = req.headers["x-trace-id"] as string;
gunsole.info({
bucket: "checkout",
message: "Processing order",
traceId,
context: { userId: req.user.id },
});
try {
const order = await db.createOrder(req.body);
gunsole.info({
bucket: "checkout",
message: "Order created",
traceId,
context: { orderId: order.id },
});
res.json(order);
} catch (err) {
gunsole.error({
bucket: "checkout",
message: "Order failed",
traceId,
context: { error: err.message },
});
res.status(500).json({ error: "Order failed" });
}
});

Both logs — frontend and backend — share the same trace ID. Filter by it in the desktop app to see the full request lifecycle.

All example apps (React, Solid, Angular) use this pattern for API calls:

const traceId = `trace-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const startTime = performance.now();
// Log at start
gunsole.info({
bucket: "api_request",
message: `Fetching: ${url}`,
tags: { api: "pokeapi", action: "fetch_start" },
traceId,
});
try {
const response = await fetch(url);
const totalTime = performance.now() - startTime;
// Log on success with timing
gunsole.info({
bucket: "api_request",
message: `Fetched successfully`,
context: { totalTimeMs: Math.round(totalTime) },
tags: { api: "pokeapi", action: "fetch_success", status: "200" },
traceId,
});
} catch (err) {
const totalTime = performance.now() - startTime;
// Log on failure with timing
gunsole.error({
bucket: "api_request",
message: `Fetch failed`,
context: { error: err.message, totalTimeMs: Math.round(totalTime) },
tags: { api: "pokeapi", action: "fetch_error" },
traceId,
});
}