Skip to content

Configure a local trace exporter, without overriding one already set - #254

Draft
jat255 wants to merge 1 commit into
jat255/wbx8-span-helpersfrom
jat255/vxv8-exporter
Draft

Configure a local trace exporter, without overriding one already set#254
jat255 wants to merge 1 commit into
jat255/wbx8-span-helpersfrom
jat255/vxv8-exporter

Conversation

@jat255

@jat255 jat255 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Third piece of M7 tracing (kata vxv8). Stacked on #246; review that first, and this base moves to main once it merges.

enable_trajectory_tracing(log=True) turns on GenAI content capture and, when nobody has installed a tracer provider, installs one that writes spans to a local file. It reports whether commons will emit conversation spans into a configured provider. Where a provider the caller installed sends them is the caller's to know, so commons does not inspect it, and tracing_configured() is named for what it can actually tell you.

Two things do not carry over from R, both checked against the Python exporter rather than assumed. FileSpanExporter appends to one path: no %N template, no trace-latest.jsonl hardlink, and it ignores OTEL_EXPORTER_OTLP_TRACES_FILE. So commons picks the filename and sets the variable itself, because a reader uses it to find the directory. And nothing in Python reads OTEL_TRACES_EXPORTER outside auto-instrumentation, so respecting an explicit exporter is a check commons has to make rather than something the SDK does.

A trace file holds whole conversations, so the default directory is per process and readable only by this user, and the files are 0600. A predictable path under the shared temporary directory would let other local users read them. This is where Python and R differ in a way that is easy to miss: R's default sits under tempdir(), which R already makes private per session, while Python's tempfile.gettempdir() is just /tmp. COMMONS_TRACES_DIR still points somewhere durable, and is what another process needs to read the files back.

Which files a reader picks up is shared with the R package, since one writes them and the other reads them, and a name outside the set fails silently: the reader finds nothing. tests/shared/traces.json pins it. The rest of the trace contract, the span names and attributes, belongs in that file too and arrives with the code that emits them (kata 4frb).

Worth a look: the concurrency handling. Installation holds a lock, reads both the exporter variable and the current provider under it, and confirms the provider it built became the global one before pointing the variable at its file. The default directory has a separate lock, because installation resolves the directory while holding the setup one. There are tests for the two windows, including one that takes the lock itself to hold a window open.

R changes

No R source changed, and no R behaviour differs. One test was added to test-trajectories.R: local_traces_pattern() is checked against the shared naming cases, replacing nothing.

The reason it is here rather than in a later PR is that this is the first time anything outside R writes files that read_local_spans() has to find. The pattern was previously exercised only incidentally, by two tests that write trace-0.jsonl and trace-latest.jsonl into a directory and count what comes back. Those still stand; this adds a direct check of the accept and reject set, including the three names nothing covered before, trace.jsonl, trace-1.jsonl.bak and traces-1.jsonl.

pkg-r/tests/testthat/fixtures/shared/traces.json is generated by scripts/sync-shared-fixtures.sh; skip it in review. The existing verify-shared-fixtures workflow fails on a stale copy.

Blast radius is one test file. Evidence the fixture drives the R suite rather than sitting beside it: mutating a case in the synced copy fails the R test, and test_file("test-trajectories.R") gives 157 passing with 5 skipped, the skips being snapshots that skip non-interactively.

Worth your scrutiny: whether trace.jsonl, with no number, should really be read. R's pattern accepts it today and I pinned current behaviour rather than changing it, but nothing commons writes produces that name, so it may be an accident worth removing on the R side.

`enable_trajectory_tracing(log=True)` turns on GenAI content capture and, when
nobody has installed a tracer provider, installs one that writes spans to a
local file. It reports whether commons will emit conversation spans into a
configured provider. Where a provider the caller installed sends them is the
caller's to know, so commons does not inspect it.

Two things do not carry over from R, both checked against
opentelemetry-exporter-otlp-json-file rather than assumed. The Python file
exporter appends to one path: no `%N` template, no `trace-latest.jsonl`
hardlink, and it ignores `OTEL_EXPORTER_OTLP_TRACES_FILE`. So commons picks
the filename, claiming it with an exclusive create, and sets the variable
after the provider is installed, because a reader uses it to find the
directory and must not be sent to a file nothing writes to. And nothing in
Python reads `OTEL_TRACES_EXPORTER` outside auto-instrumentation, so
respecting an explicit exporter means checking that variable and the installed
provider by hand.

A trace file holds whole conversations, so the default directory is created
per process, readable only by this user, and the files are `0600`. Leaving
them at a predictable path under the shared temporary directory would let
other local users read them. `COMMONS_TRACES_DIR` still points somewhere
durable, and is what another process needs to read them back.

An installed no-op provider counts as configured, not as absent, so commons
leaves it alone. `set_tracer_provider` refuses to replace it anyway, and
treating it as absent would claim a file and repoint the variable while the
provider silently stayed no-op. Installation holds a lock, reads both the
exporter variable and the provider under it, and confirms the provider it
built became the global one, so a caller configuring tracing from another
thread is not overridden and cannot leave the variable naming an empty file.
The default directory has its own lock, because installation resolves the
directory while holding the setup one.

Which files a reader picks up is shared with the R package, since one writes
them and the other reads them, and a name outside the set fails silently: the
reader finds nothing. `tests/shared/traces.json` pins it, R checks
`local_traces_pattern()` against the cases, and Python checks that every name
it chooses satisfies them. The rest of the trace contract, the span names and
attributes, belongs in that file too and arrives with the code that emits them
(kata 4frb).

Spans are written as each one ends rather than batched, because a batch is
lost when the process dies and the last turn of a conversation is the worst
span to lose.

Part of kata bvcv (M7).
@jat255
jat255 marked this pull request as draft September 2, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant