For maintainers. Using T3 Code? See docs/user.
T3 Code has one server-side observability model:
The local trace file is the persisted source of truth for normal local launches. Those launches do not
write a separate server log file, but SSH-managed launches also persist the remote process's
stdout/stderr at ~/.t3/ssh-launch/<state>/server.log.
Logs are human-facing:
Logger.consolePretty()~/.t3/ssh-launch/<state>/server.logIf you want a log message to show up in the trace file, emit it inside an active span with Effect.log.... Logger.tracerLogger will attach it as a span event.
Completed spans are written as NDJSON records to serverTracePath. The default depends on how the
server starts: production and explicitly configured homes use
<home>/userdata/logs/server.trace.ndjson (so ~/.t3/userdata/... by default, or
/custom/path/userdata/... with --home-dir /custom/path), a linked worktree dev run uses
<worktree>/.t3/userdata/logs/server.trace.ndjson, and an implicit dev run outside a linked
worktree uses ~/.t3/dev/logs/server.trace.ndjson.
Important fields common to both record types:
type: effect-span or otlp-spanname: span nametraceId, spanId, parentSpanId: correlationdurationMs: elapsed timeattributes: structured contextevents: embedded logs and custom eventseffect-span records also contain exit with Success, Failure, or Interrupted. otlp-span
records instead carry OTLP resource, scope, and optional status fields.
The TraceRecord, EffectTraceRecord, and OtlpTraceRecord schemas live in
packages/shared/src/observability.ts.
DPoP proof failures include the safe environment.dpop.failure_code span
attribute. A time_window failure means that a signed proof was too old or too
far in the future for the environment server's allowed window. It can point to
a date or time problem on either device, but it can also result from a delayed
request.
Metrics are not written to a local file.
apps/server/src/observability/Metrics.tsIf OTLP is not configured, metrics still exist in-process, but you will not have a local artifact to inspect.
Provider event NDJSON files still exist for provider runtime streams. Those are separate from the main server trace file.
There are two useful modes:
server.trace.ndjsonThe local trace file is always on. OTLP export is opt-in.
You do not need any extra env vars. Just run the app normally and inspect server.trace.ndjson.
Examples:
| 1 | npx t3 |
| 1 | node --run dev |
| 1 | node --run dev:desktop |
| 1 | docker run --name lgtm \ |
| 2 | -p 3000:3000 \ |
| 3 | -p 4317:4317 \ |
| 4 | -p 4318:4318 \ |
| 5 | --rm -ti \ |
| 6 | grafana/otel-lgtm |
Then open http://localhost:3000.
Default Grafana login:
adminadmin| 1 | export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces |
| 2 | export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics |
| 3 | export T3CODE_OTLP_SERVICE_NAME=t3-local |
Optional:
| 1 | export T3CODE_TRACE_MIN_LEVEL=Info |
| 2 | export T3CODE_TRACE_TIMING_ENABLED=true |
CLI:
| 1 | npx t3 |
Monorepo web/server dev:
| 1 | node --run dev |
Monorepo desktop dev:
| 1 | node --run dev:desktop |
Packaged desktop app:
Launch the actual app executable from the same shell so the desktop app and embedded backend inherit T3CODE_OTLP_*.
macOS app bundle example:
| 1 | T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ |
| 2 | T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ |
| 3 | T3CODE_OTLP_SERVICE_NAME=t3-desktop \ |
| 4 | "/Applications/T3 Code.app/Contents/MacOS/T3 Code" |
Direct binary example:
| 1 | T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ |
| 2 | T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ |
| 3 | T3CODE_OTLP_SERVICE_NAME=t3-desktop \ |
| 4 | ./path/to/your/desktop-app-binary |
Do not rely on launching from Finder, Spotlight, the dock, or the Start menu after setting shell env vars. Those launches usually will not pick them up.
The backend reads observability config at process start. If you change OTLP env vars, stop the app completely and start it again.
The trace file is the fastest way to inspect raw span data.
Resolve the path for the launch mode once. Production and explicitly configured homes store runtime
state under the base directory's userdata folder:
| 1 | TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson" |
A dev server started from a linked worktree defaults to that worktree's local home:
| 1 | TRACE_FILE="$WORKTREE/.t3/userdata/logs/server.trace.ndjson" |
Only an implicit dev run outside a linked worktree uses the shared dev directory:
| 1 | TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson" |
Tail the selected file:
| 1 | tail -f "$TRACE_FILE" |
Show failed spans:
| 1 | jq -c 'select(.type == "effect-span" and .exit._tag != "Success") | { |
| 2 | name, |
| 3 | durationMs, |
| 4 | exit, |
| 5 | attributes |
| 6 | }' "$TRACE_FILE" |
Show slow spans:
| 1 | jq -c 'select(.durationMs > 1000) | { |
| 2 | name, |
| 3 | durationMs, |
| 4 | traceId, |
| 5 | spanId |
| 6 | }' "$TRACE_FILE" |
Inspect embedded log events:
| 1 | jq -c 'select(any(.events[]?; .attributes["effect.logLevel"] != null)) | { |
| 2 | name, |
| 3 | durationMs, |
| 4 | events: [ |
| 5 | .events[] |
| 6 | | select(.attributes["effect.logLevel"] != null) |
| 7 | | { |
| 8 | message: .name, |
| 9 | level: .attributes["effect.logLevel"] |
| 10 | } |
| 11 | ] |
| 12 | }' "$TRACE_FILE" |
Follow one trace:
| 1 | jq -r 'select(.traceId == "TRACE_ID_HERE") | [ |
| 2 | .name, |
| 3 | .spanId, |
| 4 | (.parentSpanId // "-"), |
| 5 | .durationMs |
| 6 | ] | @tsv' "$TRACE_FILE" |
Filter orchestration commands:
| 1 | jq -c 'select(.attributes["orchestration.command_type"] != null) | { |
| 2 | name, |
| 3 | durationMs, |
| 4 | commandType: .attributes["orchestration.command_type"], |
| 5 | aggregateKind: .attributes["orchestration.aggregate_kind"] |
| 6 | }' "$TRACE_FILE" |
Filter git activity:
| 1 | jq -c 'select(.attributes["git.operation"] != null) | { |
| 2 | name, |
| 3 | durationMs, |
| 4 | operation: .attributes["git.operation"], |
| 5 | cwd: .attributes["git.cwd"], |
| 6 | hookEvents: [ |
| 7 | .events[] |
| 8 | | select(.name == "git.hook.started" or .name == "git.hook.finished") |
| 9 | ] |
| 10 | }' "$TRACE_FILE" |
Tempo is better than raw NDJSON when you want to:
traceIdRecommended flow in Grafana:
Explore.Tempo data source.Last 15 minutes.Good first searches:
t3-local, t3-dev, or t3-desktopsendTurn or a Git operation such as GitVcsDriver.statusDetails.statusgit.operation attribute identifies the operationorchestration.command_typeOnce you know traces are arriving, narrower TraceQL queries for names such as sendTurn or Git
operation names become useful.
Traces are best for one request. Metrics are best for trends.
Good metric families to watch:
t3_rpc_request_durationt3_orchestration_command_durationt3_orchestration_command_ack_durationt3_provider_turn_durationt3_git_command_durationCounters tell you volume and failure rate:
t3_rpc_requests_totalt3_orchestration_commands_totalt3_provider_turns_totalt3_git_commands_totalUse metrics when the question is:
Use traces when the question is:
t3_orchestration_command_ack_duration measures:
That is a server-side acknowledgment metric. It does not measure:
If you need those later, add client-side instrumentation or a dedicated server fanout metric.
effect-span records where exit._tag != "Success".traceId.t3_orchestration_command_ack_duration by commandType.git.operation spans.git.hook.started and git.hook.finished events.Usually one of these is true:
T3CODE_OTLP_TRACES_URL was not setIf the local NDJSON file is updating, local tracing is working. The problem is almost always OTLP export configuration or process startup.
Good span boundaries:
Avoid tracing every tiny helper. Most helpers should inherit the active span rather than create a new one.
Effect.fn(...) Where It Already ExistsThe codebase already uses Effect.fn("name") heavily. That should usually be your first tracing boundary.
For ad hoc work:
| 1 | import { Effect } from "effect"; |
| 2 | |
| 3 | const runThing = Effect.gen(function* () { |
| 4 | yield* Effect.annotateCurrentSpan({ |
| 5 | "thing.id": "abc123", |
| 6 | "thing.kind": "example", |
| 7 | }); |
| 8 | |
| 9 | yield* Effect.logInfo("starting thing"); |
| 10 | return yield* doWork(); |
| 11 | }).pipe(Effect.withSpan("thing.run")); |
Use span annotations for IDs, paths, and other detailed context:
| 1 | yield * |
| 2 | Effect.annotateCurrentSpan({ |
| 3 | "provider.thread_id": input.threadId, |
| 4 | "provider.request_id": input.requestId, |
| 5 | "git.cwd": input.cwd, |
| 6 | }); |
Good metric labels:
Bad metric labels:
Detailed context belongs on spans, not metrics.
Logs inside a span become part of the trace story:
| 1 | yield * Effect.logInfo("starting provider turn"); |
| 2 | yield * Effect.logDebug("waiting for approval response"); |
Those messages show up as span events because Logger.tracerLogger is installed.
withMetrics(...) is the default way to attach a counter and timer to an effect:
| 1 | import { someCounter, someDuration, withMetrics } from "../observability/Metrics.ts"; |
| 2 | |
| 3 | const program = doWork().pipe( |
| 4 | withMetrics({ |
| 5 | counter: someCounter, |
| 6 | timer: someDuration, |
| 7 | attributes: { |
| 8 | operation: "work", |
| 9 | }, |
| 10 | }), |
| 11 | ); |
The server observability layer is assembled in apps/server/src/observability/Layers/Observability.ts.
It provides:
Logger.tracerLoggerLocal trace file:
T3CODE_TRACE_FILE: override trace file pathT3CODE_TRACE_MAX_BYTES: per-file rotation size, default 10485760T3CODE_TRACE_MAX_FILES: rotated file count, default 10T3CODE_TRACE_BATCH_WINDOW_MS: flush window, default 200T3CODE_TRACE_MIN_LEVEL: minimum trace level, default InfoT3CODE_TRACE_TIMING_ENABLED: enable timing metadata, default trueOTLP export:
T3CODE_OTLP_TRACES_URL: OTLP trace endpointT3CODE_OTLP_METRICS_URL: OTLP metric endpointT3CODE_OTLP_EXPORT_INTERVAL_MS: export interval, default 10000T3CODE_OTLP_SERVICE_NAME: service name, default t3-serverT3CODE_OTLP_HEADERS: extra headers for both exporters, same format as
OTEL_EXPORTER_OTLP_HEADERS: comma-separated key=value pairs with percent-encoded values.T3CODE_OTLP_PROTOCOL: http/json (default) or http/protobufIf the OTLP URLs are unset, local tracing still works and metrics stay in-process only.
Current high-value span and metric boundaries include:
effect/rpcapps/server/src/observability/RpcInstrumentation.tsserverLogPath still exists in config for compatibility, but the trace file is the primary
structured persisted artifact