Send Spans from a Product

You need to record trace spans from within a product -- what happened, how long it took, whether it succeeded -- and trust that those spans are stored and queryable afterward. This page walks through the bounded task of connecting to the trace service, building a span, sending it, and querying it back to confirm the round trip.

For the full setup including architecture context, the query interface, and tree reconstruction, see Collect Trace Spans from Any Product.

Prerequisites

Connect

import { createClient } from "@forwardimpact/librpc";
import { createLogger } from "@forwardimpact/libtelemetry";
import { trace } from "@forwardimpact/libtype";

const logger = createLogger("my-product");
const traceClient = await createClient("trace", logger);

Send a span

Build a trace.Span and call RecordSpan. Every span needs a trace_id (grouping related spans) and a span_id (unique to this span):

const span = trace.Span.fromObject({
  trace_id: "eval-run-042",
  span_id: "step-01",
  name: "generate-output",
  kind: 1,  // INTERNAL
  start_time_unix_nano: BigInt(Date.now()) * 1_000_000n,
  end_time_unix_nano: BigInt(Date.now() + 2000) * 1_000_000n,
  attributes: {
    "agent.name": "release-engineer",
    "step.type": "generation",
  },
  status: { code: 1, message: "" },  // OK
  resource: {
    attributes: { "service.name": "my-product" },
  },
});

const result = await traceClient.RecordSpan(span);
console.log("Sent:", result.success);

Expected output:

Sent: true

Send a child span

Link a child span to its parent using parent_span_id:

const childSpan = trace.Span.fromObject({
  trace_id: "eval-run-042",
  span_id: "step-02",
  parent_span_id: "step-01",
  name: "verify-output",
  kind: 1,
  start_time_unix_nano: BigInt(Date.now()) * 1_000_000n,
  end_time_unix_nano: BigInt(Date.now() + 800) * 1_000_000n,
  attributes: {
    "agent.name": "release-engineer",
    "step.type": "verification",
    "verdict": "pass",
  },
  status: { code: 1, message: "" },
  resource: {
    attributes: { "service.name": "my-product" },
  },
});

await traceClient.RecordSpan(childSpan);

Send an error span

When a step fails, set the status code to ERROR (2) with a message:

const errorSpan = trace.Span.fromObject({
  trace_id: "eval-run-042",
  span_id: "step-03",
  parent_span_id: "step-01",
  name: "publish-results",
  kind: 1,
  start_time_unix_nano: BigInt(Date.now()) * 1_000_000n,
  end_time_unix_nano: BigInt(Date.now() + 500) * 1_000_000n,
  attributes: {
    "agent.name": "release-engineer",
  },
  status: { code: 2, message: "Connection refused on port 3005" },
  resource: {
    attributes: { "service.name": "my-product" },
  },
});

await traceClient.RecordSpan(errorSpan);

Query to confirm

After sending spans, query them back by trace ID to confirm storage:

const query = trace.QueryRequest.fromObject({
  filter: { trace_id: "eval-run-042" },
});

const result = await traceClient.QuerySpans(query);
console.log("Stored spans:", result.spans?.length ?? 0);

for (const s of result.spans ?? []) {
  const status = s.status?.code === 2 ? "ERROR" : "OK";
  console.log(`  ${s.name} [${status}]`);
}

Expected output:

Stored spans: 3
  generate-output [OK]
  verify-output [OK]
  publish-results [ERROR]

Handle send failures

RecordSpan validates that trace_id and span_id are present. Missing either produces a gRPC error:

try {
  const bad = trace.Span.fromObject({
    trace_id: "",
    span_id: "orphan",
    name: "missing-trace-id",
  });
  await traceClient.RecordSpan(bad);
} catch (err) {
  console.error(err.message);
  // "trace_id is required"
}

If the trace service is unreachable, the client retries up to 10 times with a 1-second delay before surfacing the connection error.

Verify

You have reached the outcome of this guide when:

  • RecordSpan with a valid trace_id and span_id returns { success: true }.
  • Child spans reference their parent and appear in the same trace query.
  • Error spans preserve their status code and message.
  • QuerySpans returns all spans sent under the same trace_id.