Skip to content

fix(telemetry): upload evals when recording is fully disabled - #7935

Open
BlueX888 wants to merge 1 commit into
livekit:mainfrom
BlueX888:fix/recording-disabled-drops-evaluations
Open

BlueX888 wants to merge 1 commit into
livekit:mainfrom
BlueX888:fix/recording-disabled-drops-evaluations

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

fix(telemetry): upload evals when recording is fully disabled

What breaks

When a session runs with every recording option off (record=False with no logs/metrics/traces), JobContext._prepare_telemetry() never registers the job and never creates the log pipeline β€” so session report data (evaluations, tags, outcome) set on the session is silently dropped: nothing is exported at session end. Recording options should govern trace/log/metric volume, not the session report, which is an explicit product feature (session.evaluations, session.tags, session.outcome).

Fix

  • traces.py: _GatedLogExporter now passes the session-report scopes (chat_history, evaluations) for registered jobs even when the job's logs option is off; configure() gains enable_session_report (creates the log pipeline without Python-log export) and enable_metrics; _setup_cloud_tracer forwards both.
  • job.py: the all-off case with a cloud observability URL runs init_recording() β†’ _setup_cloud_tracer(enable_traces=False, enable_logs=False, enable_session_report=True, enable_metrics=False), registering the job and creating the log pipeline. Traces, Python logs and metric providers stay off; _on_cleanup flushes the batch processor while registered, then unregisters.

Tests

tests/test_session_report_recording_disabled.py: drives the real JobContext._prepare_telemetry() β†’ init_recording(_RECORDING_ALL_OFF) β†’ _on_session_end() with a synchronous InMemoryLogRecordExporter (no network); asserts the exported bodies are exactly ["evaluation", "outcome", "tag"] and that ordinary Python logs are not exported.

  • On base: fails β€” exported records: [] (everything dropped; reproduced in both a fresh process and one where a prior job had created the pipeline).
  • With the fix: passes (5/5 repeats); recording/telemetry neighbors 82 + 242 + 47 passed; traces/logs/metrics verified off (no SdkMeterProvider / metric exporter created); ruff + type checks clean on the changed files.

Relationship to open PR #7836

#7836 covers the sibling case (logs=False but configure() ran). This change covers the all-off case where configure() is never reached. They are complementary, but they share the traces.py surface: both introduce a session-report scope set and the same gate/configure() hunks, so whichever lands first, the other needs a mechanical rebase. A clean reconciliation would keep one shared scope constant and one gate check, make log-pipeline creation unconditional in configure(), and keep enable_metrics (then #7836's dedicated flag can be dropped).

Known non-blocking edges (documented in review): OTel derives instrumentation scope from the logger name, so a user logger named exactly evaluations/chat_history could pass the allowlist (no framework logger uses those names); and the policy-wide gate means a logs=False job's session report is uploaded only when a process log pipeline already exists β€” the exact gap #7836 fills.

JobContext.init_recording returned before _setup_cloud_tracer for all-off
options, so no logger provider, log pipeline, or job registration existed.
_upload_session_report still emitted evaluations, tag metadata, and the
outcome from _on_session_end's has_evals branch, but those records landed
in a no-op provider (fresh process) or were dropped by the log gate
(reused process).

Configure a report-only pipeline for all-off jobs: enable the log pipeline
without Python log export, traces, or metrics, and let session-report logger
scopes through the gate for a registered job even when logs are disabled.
@BlueX888
BlueX888 requested a review from a team as a code owner October 18, 2026 21:48

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

1 flag not posted on this PR by your GitHub settings β€” view it in Devin Review. (Configure)

Devin Review

Comment on lines +545 to +547
def _is_session_report_record(record: ReadableLogRecord) -> bool:
scope = getattr(record, "instrumentation_scope", None)
return getattr(scope, "name", None) in _SESSION_REPORT_SCOPES

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled Python logs still upload

When a shared handler captures a record=False job's Python log named evaluations, _is_session_report_record accepts it. The log reaches Cloud despite logs_enabled=False.

Learn more

The cloud log pipeline is shared between jobs. When another job has enabled Python logging, its root handler also sees Python logs from a job with recording disabled. OpenTelemetry assigns the logger name as the instrumentation scope, so the name evaluations satisfies this allowlist even for a Python log. The exporter then forwards the log because the job is registered, although its logs_enabled flag is false. Only records emitted by _upload_session_report must bypass that flag; a scope name alone cannot establish their origin.

Example: Job A records logs and leaves the root OTLP handler attached. Concurrent job B starts with record=False and logs logging.getLogger("evaluations").warning("ordinary diagnostic"). B's log uploads to Cloud instead of remaining disabled.

Recommended fix: Give _upload_session_report records a dedicated internal attribute and allow only those marked records through _GatedLogExporter when logs_enabled=False. Do not use public logger scope names as proof of origin.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

enable_traces=False,
enable_logs=False,
enable_session_report=True,
enable_metrics=False,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled sessions still export shared metrics

When another job has installed the process-wide meter, enable_metrics=False leaves that meter active for record=False jobs. Their measurements still upload because _job_attrs stamps job identity without gating export.

Learn more

Metric instruments in otel_metrics bind to the OpenTelemetry global meter, which outlives individual jobs. Skipping _ensure_meter_provider only prevents creating a meter when none exists; it cannot turn off one installed by a previous or concurrent job. The metric helpers continue recording measurements with the disabled job's identity, and the existing meter's reader continues exporting them. This affects reused workers and concurrent jobs, not the fresh-process test.

Example: Job A enables recording and creates the Cloud meter provider. After Job A, Job B starts with record=False. A usage counter recorded by B still flows through A's meter provider to Cloud even though B passed enable_metrics=False.

Recommended fix: Gate metric recording or exporting per job using its resolved recording state, while preserving metrics for other jobs sharing the meter. Add a reused-worker test covering a recorded job followed by an all-off job.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +920 to +929
self._telemetry_state = _setup_cloud_tracer(
room_id=self.job.room.sid,
job_id=self.job.id,
agent_name=self.job.agent_name,
observability_url=obs_url,
enable_traces=False,
enable_logs=False,
enable_session_report=True,
enable_metrics=False,
metadata=self._otel_metadata(options, redaction_enabled=redaction_enabled),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Redacted sessions export unfiltered judge reasoning

With record=False and redaction enabled, a judge evaluation can contain conversation text in its reasoning. The evaluation log uses an unmarked attribute, so PII filtering preserves and uploads that text.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +920 to +929
self._telemetry_state = _setup_cloud_tracer(
room_id=self.job.room.sid,
job_id=self.job.id,
agent_name=self.job.agent_name,
observability_url=obs_url,
enable_traces=False,
enable_logs=False,
enable_session_report=True,
enable_metrics=False,
metadata=self._otel_metadata(options, redaction_enabled=redaction_enabled),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Redacted sessions export unfiltered tag metadata

With record=False, redaction enabled, and an evaluation, tag metadata containing conversation content now uploads. The tag log lacks a PII marker, so the redaction processor preserves it.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

This branch has not been deployed

No deployments
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