Emitting Custom OTLP Attributes
Shared agentic workflow imports can emit their own OTLP spans alongside the built-in gh-aw telemetry. This lets third-party tools — APM agents, data pipeline steps, custom scanners — attach their own measurements to the same distributed trace that gh-aw creates for each workflow run.
Quick start
Section titled “Quick start”The otlp.cjs helper provides a minimal, stable API. Use it in any steps: entry of a shared import:
---# My Tool — shared import that instruments its own telemetry
steps: - name: My Tool — do work and record telemetry id: my-tool-run uses: actions/github-script@v8 with: script: | const otlp = require('/tmp/gh-aw/actions/otlp.cjs');
const startMs = Date.now(); // ── do your tool's work here ────────────────────────────────────── // const result = await myTool.run(); // ───────────────────────────────────────────────────────────────── const endMs = Date.now();
await otlp.logSpan('my-tool', { 'my-tool.version': '1.2.3', 'my-tool.items_processed': 42, 'my-tool.result': 'success', }, { startMs, endMs });---
My tool has run and its telemetry span will appear in the same distributed trace as the workflow run.Import the shared file in any workflow alongside the OTLP configuration:
---on: schedule: dailyengine: copilotimports: - shared/observability-otlp.md # sets the OTLP endpoint + auth headers - shared/my-tool.md # runs my-tool and records its span---
# Daily Report
Run the daily report using my-tool results.That is the complete integration. The otlp.cjs helper reads all required environment variables automatically — endpoint, trace ID, parent span ID — so no additional configuration is needed in the step.
logSpan API
Section titled “logSpan API”const otlp = require('/tmp/gh-aw/actions/otlp.cjs');
await otlp.logSpan(toolName, attributes, options);| Parameter | Type | Description |
|---|---|---|
toolName | string | Logical name for the tool (e.g. "my-scanner"). Used as service.name and as the span name prefix <toolName>.run. |
attributes | Record<string, string | number | boolean> | Domain-specific attributes emitted on the span. All env plumbing is handled automatically. |
options.startMs | number | Span start time (ms since epoch). Defaults to Date.now(). |
options.endMs | number | Span end time (ms since epoch). Defaults to Date.now(). |
options.isError | boolean | When true, sets the span status to ERROR. |
options.errorMessage | string | Human-readable status message included when isError is true. |
options.traceId | string | Override trace ID. Defaults to GITHUB_AW_OTEL_TRACE_ID. |
options.parentSpanId | string | Override parent span ID. Defaults to GITHUB_AW_OTEL_PARENT_SPAN_ID. |
options.endpoint | string | Override OTLP endpoint. Defaults to OTEL_EXPORTER_OTLP_ENDPOINT. |
logSpan is non-fatal and never throws. Export failures are surfaced as console.warn. When GITHUB_AW_OTEL_TRACE_ID is missing or invalid, the call returns silently — no warning, no side-effects.
Recording an error span
Section titled “Recording an error span”await otlp.logSpan('my-scanner', { 'my-scanner.items_scanned': 100,}, { isError: true, errorMessage: 'database connection timed out' });Attribute naming recommendations
Section titled “Attribute naming recommendations”- Use
your-tool.as a prefix for tool-specific attributes (e.g.my-tool.items_processed). - Use OpenTelemetry semantic conventions for cross-cutting concerns (e.g.
db.system,http.response.status_code). - Avoid attribute names containing
token,secret,password,key, orauth— the helpers automatically redact matching attribute values before sending.
Security
Section titled “Security”Attribute values are sanitized automatically before the payload is exported or mirrored:
- Redacts the value of any attribute whose key matches
token,secret,password,passwd,key,auth,credential,api-key, oraccess-key(case-insensitive), replacing it with[REDACTED]. - Truncates string values longer than 1,024 characters.
Sanitization is applied to both the over-the-wire OTLP export and the local JSONL debug mirror, so you do not need to call it yourself.
Debugging without a live collector
Section titled “Debugging without a live collector”Every span is always appended as a sanitized JSON line to /tmp/gh-aw/otel.jsonl, even when OTEL_EXPORTER_OTLP_ENDPOINT is not set. This file is included in the firewall-audit-logs artifact so you can inspect spans after the run:
# Download firewall/telemetry artifacts for a rungh aw logs <run-id> --artifacts firewall
# Inspect spans emitted by your toolcat otel.jsonl | jq 'select(.resourceSpans[].scopeSpans[].spans[].name | startswith("my-tool"))'Advanced: low-level API
Section titled “Advanced: low-level API”For full control — multiple linked spans, custom resource attributes, or span events — use the underlying helpers from send_otlp_span.cjs directly. The key environment variables set by the actions/setup step are:
| Variable | Description |
|---|---|
GITHUB_AW_OTEL_TRACE_ID | 32-char hex trace ID shared by all spans in this run. |
GITHUB_AW_OTEL_PARENT_SPAN_ID | 16-char hex span ID of the job setup span; use as parentSpanId to nest spans under it. |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector base URL. |
OTEL_EXPORTER_OTLP_HEADERS | Comma-separated key=value authentication headers. |
const { buildAttr, buildOTLPPayload, sendOTLPSpan, generateSpanId, SPAN_KIND_CLIENT,} = require('/tmp/gh-aw/actions/send_otlp_span.cjs');
const traceId = process.env.GITHUB_AW_OTEL_TRACE_ID;const parentSpanId = process.env.GITHUB_AW_OTEL_PARENT_SPAN_ID;const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
const setupSpanId = generateSpanId();const querySpanId = generateSpanId();
// Parent span for the overall operationawait sendOTLPSpan(endpoint, buildOTLPPayload({ traceId, spanId: setupSpanId, parentSpanId, spanName: 'my-tool.setup', startMs: t0, endMs: t1, serviceName: 'my-tool', kind: SPAN_KIND_CLIENT, attributes: [buildAttr('my-tool.phase', 'setup')], resourceAttributes: [buildAttr('my-tool.version', '1.2.3')],}));
// Child span nested under the parent span aboveawait sendOTLPSpan(endpoint, buildOTLPPayload({ traceId, spanId: querySpanId, parentSpanId: setupSpanId, spanName: 'my-tool.query', startMs: t1, endMs: t2, serviceName: 'my-tool', kind: SPAN_KIND_CLIENT, attributes: [buildAttr('my-tool.query.rows', 1234)],}));Related documentation
Section titled “Related documentation”- Observability (
observability:) — configure the OTLP endpoint and headers - Imports — how shared workflow imports work
- Deterministic Agentic Patterns — adding custom
steps:to workflows - Artifacts — downloading the
otel.jsonlmirror and other artifacts