Monitor usage with OpenTelemetry

Syntic Code can send a stream of events and usage counters to the monitoring stack you already run. Point it at an OpenTelemetry collector, a Prometheus server, or just your terminal, and you can chart sessions, tokens and estimated spend per team, model or repository.

Export is off by default. Nothing is sent anywhere until you turn it on, and the destination is always one you choose.

How it works

When a session starts, Syntic Code reads a handful of environment variables. If export is switched on and at least one exporter is named, it starts collecting:

  • Events (the OpenTelemetry logs signal): one record per prompt, tool decision, tool result, model request, model error and hook run.
  • Metrics: counters for sessions, estimated cost and tokens.

Events are buffered and sent in batches. A batch leaves every 5 seconds by default, as soon as 100 events are waiting, and once more when the session ends, so short -p runs are not lost. Metrics are sent as delta sums every 60 seconds by default, and at exit.

Three things must line up for anything to be exported:

  1. SYNTIC_ENABLE_TELEMETRY is on (1, true, yes or on).
  2. OTEL_LOGS_EXPORTER and/or OTEL_METRICS_EXPORTER names an exporter.
  3. The privacy level is standard. Any setting that lowers it (SYNTIC_PRIVACY=essential, SYNTIC_OFFLINE=1, SYNTIC_DISABLE_NONESSENTIAL_TRAFFIC=1, SYNTIC_TELEMETRY=0, DO_NOT_TRACK=1 or DISABLE_AUTOUPDATER=1) switches export off, whatever else is set. See Data usage.

Quick start

Turn export on

export SYNTIC_ENABLE_TELEMETRY=true

Pick exporters

Choose one for events and one for metrics. You can set only one of the two.

export OTEL_LOGS_EXPORTER=otlp      # or console
export OTEL_METRICS_EXPORTER=otlp   # or console, or prometheus

Point at your collector

export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.internal.example:4318
export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer%20<token>"

Try it with short intervals

While testing, send batches every second so you see data quickly. Remove these once it works.

export OTEL_LOGS_EXPORT_INTERVAL=1000
export OTEL_METRIC_EXPORT_INTERVAL=1000
syntic -p "list the files in this folder"

To check the setup without a collector, use the console exporter. Each batch is printed to standard error as one line of JSON, wrapped as {"otel": "logs" | "metrics", "data": …}:

SYNTIC_ENABLE_TELEMETRY=1 OTEL_LOGS_EXPORTER=console \
  syntic -p "say hello" 2> otel.jsonl

Use console with -p runs. In the interactive screen, output on standard error would draw over the interface.

Configuration reference

VariableWhat it doesDefault
SYNTIC_ENABLE_TELEMETRYMaster switch for export.off
OTEL_LOGS_EXPORTERExporter for events: otlp or console (logging is accepted as another name for console).none
OTEL_METRICS_EXPORTERExporter for metrics: otlp, console or prometheus.none
OTEL_EXPORTER_OTLP_ENDPOINTBase address of the collector. /v1/logs and /v1/metrics are added to it.http://localhost:4318
OTEL_EXPORTER_OTLP_LOGS_ENDPOINTFull URL for events. Used as given, nothing is added.
OTEL_EXPORTER_OTLP_METRICS_ENDPOINTFull URL for metrics. Used as given.
OTEL_EXPORTER_OTLP_HEADERSHeaders sent with every batch, as key=value,key2=value2. Values are percent-decoded, so write a space as %20.
OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_HEADERSExtra headers for one signal only, added after the shared ones.
OTEL_EXPORTER_OTLP_PROTOCOLWire protocol. Only http/json is supported.http/json
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, OTEL_EXPORTER_OTLP_METRICS_PROTOCOLThe same, for one signal.
OTEL_LOGS_EXPORT_INTERVALMilliseconds between event batches (100 or more).5000
OTEL_METRIC_EXPORT_INTERVALMilliseconds between metric batches (100 or more).60000
OTEL_RESOURCE_ATTRIBUTESYour own attributes on every record, key=value,…, percent-decoded.
OTEL_LOG_USER_PROMPTSInclude the prompt text in syntic.user_prompt events.off
OTEL_LOG_TOOL_DETAILSInclude tool arguments in syntic.tool_result events.off
OTEL_INCLUDE_SESSION_IDAdd session.id to every record.off
SYNTIC_OTEL_MAX_CONTENT_CHARSLongest text kept in any field; longer text ends in …[truncated].2048
OTEL_EXPORTER_PROMETHEUS_HOSTAddress the Prometheus endpoint listens on.127.0.0.1
OTEL_EXPORTER_PROMETHEUS_PORTPort of the Prometheus endpoint.9464
SYNTIC_PROMETHEUS_ALLOW_REMOTEAllow a Prometheus host other than loopback.off
SYNTIC_PROMETHEUS_FILEWrite the Prometheus text to this file on every metrics interval.

A choice of grpc or http/protobuf turns that signal off and logs a warning, rather than sending data your collector cannot read. Choosing prometheus for events does the same, since Prometheus carries metrics only. Batches that fail to send are dropped quietly and never interrupt your session.

Roll it out to a team

The variables only take effect from places you control:

  • Your shell or your deployment tooling. Export them in a login profile, a CI job, or a wrapper script.
  • User settings. The env block in ~/.syntic/settings.json can carry them.
  • Organization policy. Put them in the env block of a managed settings file, so every user on the machine reports to the same collector.

A repository can not set any OTEL_* variable, SYNTIC_ENABLE_TELEMETRY or any SYNTIC_TELEMETRY* variable through its .syntic/settings.json. Those are dropped, so a cloned project cannot quietly switch reporting on or redirect it to a host of its choosing.

A managed policy that sends everything to one collector might look like this:

{
  "env": {
    "SYNTIC_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.internal.example:4318",
    "OTEL_RESOURCE_ATTRIBUTES": "department=platform,cost_center=eng-42"
  }
}

Split by team

Use OTEL_RESOURCE_ATTRIBUTES to tag records with whatever you group by. Give each team its own policy file in settings.policy.d/, or set the variable in each team’s shell profile:

export OTEL_RESOURCE_ATTRIBUTES="team=payments,department=engineering"

Your attributes are added last, so they can also replace a built-in attribute such as service.name if your collector expects a different value.

What is exported

Resource attributes

Every event and metric carries these:

AttributeValue
service.namesyntic-code
service.versionThe CLI version.
os.typemacos, linux or windows.
host.archThe processor architecture, such as aarch64 or x86_64.
vcs.repository.nameThe name of the folder that holds .git, when the session runs inside a repository.
session.idThe session id, only with OTEL_INCLUDE_SESSION_ID.
Your attributesEverything in OTEL_RESOURCE_ATTRIBUTES.

Session ids are left out by default because every session would become its own series, which makes metrics expensive to store.

Events

Each event is a log record with severity INFO, a body equal to the event name, and an event.name attribute with the same value.

EventWhenAttributes
syntic.user_promptYou send a prompt in the main conversation (prompts a subagent receives are not counted).prompt_length; prompt only with OTEL_LOG_USER_PROMPTS.
syntic.tool_decisionA tool call is allowed or refused.tool_name, decision (accept or reject), source (allowed, or policy for a refusal).
syntic.tool_resultA tool call finishes.tool_name, success, duration_ms, error when it failed; tool_parameters only with OTEL_LOG_TOOL_DETAILS.
syntic.api_requestA model request completes.model, cost_usd, duration_ms, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens.
syntic.api_errorA request ends in an error after its retries.model, error, attempt.
syntic.hook_executionA hook finishes.hook_event (such as PreToolUse), outcome (answered or no_answer), duration_ms.

cost_usd is the same estimate the CLI shows in /cost: list price, or your organization’s modelPricing rates if a policy sets them. See Manage costs.

Metrics

All metrics are monotonic sums with delta temporality: each batch holds what was counted since the previous one.

MetricUnitAttributesCounts
syntic.session.countsessionsnoneOne per session start.
syntic.cost.usageUS dollarsmodelEstimated cost of each model request.
syntic.token.usagetokenstype (input, output, cacheRead, cacheCreation), modelTokens per request; a type with zero tokens is not recorded.

Prometheus

With OTEL_METRICS_EXPORTER=prometheus, metrics are kept as running totals instead of being pushed. Names follow Prometheus rules: dots become underscores and counters end in _total, so syntic.token.usage becomes syntic_token_usage_total. Attributes become labels.

There are two ways to collect them:

ModeSetResult
Scrape endpointnothing extra, or OTEL_EXPORTER_PROMETHEUS_HOST / _PORThttp://127.0.0.1:9464/metrics serves the totals while the session runs.
Text fileSYNTIC_PROMETHEUS_FILE=/var/lib/node_exporter/syntic.promThe file is rewritten on every metrics interval, ready for a textfile collector. No port is opened unless you also set a port.

The endpoint only listens on a loopback address unless you set SYNTIC_PROMETHEUS_ALLOW_REMOTE=1, so usage numbers do not become visible to the network by accident. A host that is not loopback, without that switch, turns metrics off with a warning.

Because each session is its own process, the scrape endpoint suits a long interactive session on one machine. For many short runs, the text file or the OTLP exporter works better.

Example collector setup

A minimal OpenTelemetry Collector configuration that accepts what Syntic Code sends over HTTP and prints it:

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
 
exporters:
  debug:
    verbosity: detailed
 
service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      exporters: [debug]

Swap the debug exporter for your log store and metrics backend once you see records arriving.

Traces from CI and wrapper scripts

When Syntic Code is started inside a traced job, pass the job’s span in the standard TRACEPARENT variable (and TRACESTATE if you use it). Two things happen:

  • Every exported event carries that trace id, so records line up with the rest of the job in your tracing tool.
  • Each shell command the agent runs gets its own TRACEPARENT: the same trace id with a fresh span id. Tools in that command that understand trace context can attach their spans under the agent’s run.

A malformed TRACEPARENT, or one with an all-zero trace or span id, is ignored.

Privacy

  • Prompt text and tool arguments are not exported unless you opt in with OTEL_LOG_USER_PROMPTS and OTEL_LOG_TOOL_DETAILS. Without them, only the prompt’s length and the tool’s name are sent.
  • Error messages are exported, cut to SYNTIC_OTEL_MAX_CONTENT_CHARS.
  • Your sign-in credential is never included.
  • Export goes only to the endpoints you configure. Syntic does not receive a copy.

Troubleshooting

SymptomCheck
Nothing arrivesIs SYNTIC_ENABLE_TELEMETRY set, and an exporter named? Is the privacy level standard? syntic doctor shows the level on its Network line.
The variables are in .syntic/settings.json but nothing happensProject settings cannot set them. Move them to your shell, user settings or a managed policy.
Events arrive but no metrics, or the reverseEach signal has its own exporter variable. Also check that the protocol is http/json.
Data shows up a minute lateMetrics go out every 60 seconds. Lower OTEL_METRIC_EXPORT_INTERVAL while testing.
The Prometheus endpoint never opensThe port may be taken, or the host is not loopback without SYNTIC_PROMETHEUS_ALLOW_REMOTE. Run with --debug to see the warning.
Too many series in your metrics storeLeave OTEL_INCLUDE_SESSION_ID off and keep OTEL_RESOURCE_ATTRIBUTES to a few stable values.