Skip to content

FE-1221, SRE-810: Add structured correlated logging across the Petrinaut optimization pipeline#9059

Merged
kube merged 13 commits into
mainfrom
cf/fe-1221-petrinaut-optimization-logging
Jul 23, 2026
Merged

FE-1221, SRE-810: Add structured correlated logging across the Petrinaut optimization pipeline#9059
kube merged 13 commits into
mainfrom
cf/fe-1221-petrinaut-optimization-logging

Conversation

@kube

@kube kube commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Make an optimization request traceable across NodeAPI and petrinaut-opt, with enough boundary logging to diagnose the intermittent staging network error.

This PR now configures OpenTelemetry, exporters, tracing providers, RED metrics, or CLI diagnostics since yz/sre-810-otlp-for-petrinaut-opt was merged here. This PR supplies a small set of correlation attributes and lifecycle events that its OTEL logging bridge can export.

🔗 Related links

🤝 Scope boundary with the OTEL branch

Concern This PR SRE-810 OTEL branch
Correlate NodeAPI → optimizer Consumes the resulting attributes
NodeAPI capacity, stream-open, and terminal outcome logs
Python capacity, initialization-failure, and study lifecycle records ✅, using stdlib logging Exports stdlib records through OTEL
CLI diagnostics or stderr forwarding into logs
FastAPI/request and Optuna trial spans
OTLP traces, metrics, and logs
OTEL SDK/exporter dependencies and setup
OTEL environment variables Adds none Must use standard OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME, and other standard OTEL_* variables per H-5069

At OTEL-branch commit fcf1d0ce72, a three-way merge with this PR succeeds without conflicts. The OTEL branch still uses HASH_OTLP_ENDPOINT at that commit; changing it to standard OTEL variables remains SRE-810 work.

This PR was trimmed to avoid overlap: it contains no Python logging-handler setup, JSON formatter, manual traceparent parsing, custom OTEL variables, per-trial instrumentation, CLI JSON diagnostics, or CLI stderr log forwarding.

🪪 Correlation identifiers and variables

Name Kind / exposure Produced by Consumed by Purpose
requestId NodeAPI log field Existing NodeAPI request-id middleware NodeAPI application logger Correlates NodeAPI records for one HTTP request
x-hash-request-id Internal HTTP request header NodeAPI petrinaut-opt Carries the NodeAPI request id across the service boundary
request_id Python log attribute petrinaut-opt from x-hash-request-id Logging/OTEL handler Joins optimizer lifecycle records back to NodeAPI
run_id Python status id and log attribute petrinaut-opt Status API and Python lifecycle logs Identifies one admitted Optuna study
X-Optimization-Run-ID HTTP response header, exposed through NodeAPI petrinaut-opt Optimizer client, NodeAPI, browser Returns the optimizer-owned run id on success and pre-stream failures
optimizationRunId TypeScript field Optimizer client from X-Optimization-Run-ID NodeAPI terminal logs Uses a readable TypeScript name for the optimizer run id

The two correlation chains are:

requestIdx-hash-request-id → Python request_id

run_idX-Optimization-Run-ID → NodeAPI/browser optimizationRunId

🔍 What does this change?

  • NodeAPI: uses the existing application logger and includes requestId as ordinary metadata; logs local capacity rejections, stream-open, client disconnect, and one terminal outcome with durationMs, outcome, and optimizationRunId.
  • Optimizer client: forwards x-hash-request-id and returns { events, optimizationRunId }, including the run id from failed upstream responses.
  • petrinaut-opt: returns X-Optimization-Run-ID and attaches request_id and run_id to bounded capacity, initialization-failure, study-start, completion, failure, disconnect, and worker-timeout records.
  • Privacy: new lifecycle records contain no manifest, raw request body, validation details, user id, user-authored code, or raw upstream error message.
  • CLI boundary: the existing stderr pipe is still drained to prevent the child blocking, but its contents are discarded. This PR adds no CLI logging protocol or child-process correlation variable.
  • Correctness fixes: terminal upstream error events are logged as failed rather than completed; a timeout or disconnect during final-write backpressure cannot overwrite an already-committed terminal outcome; and the Petrinaut website consumes the optimizer client’s new stream handle shape.

🧾 What is logged at each step?

Layer Step / event Level Stable fields Why it is logged
NodeAPI Account already has an active optimization warn requestId, activeOptimizationCount Explains the local per-account 429 rejection.
NodeAPI NodeAPI optimization capacity reached warn requestId, active/max counts Explains the local service-wide 429 rejection.
NodeAPI Optimizer stream opened info requestId, optimizationRunId Confirms that upstream accepted the request and returned headers.
NodeAPI Optimization completed info requestId, optimizationRunId, durationMs, outcome=completed Records successful completion at the browser-facing boundary.
NodeAPI Optimizer returned a terminal error event warn requestId, optimizationRunId, durationMs, outcome=upstream-error Records a study failure that was delivered normally through the stream.
NodeAPI Client disconnected info requestId, optimizationRunId, durationMs, outcome=client-disconnected Distinguishes downstream cancellation from an optimizer failure.
NodeAPI Request failed or timed out warn requestId, optimizationRunId, durationMs, outcome, errorType, timeoutKind, upstreamStatus Identifies the failing boundary without logging the raw upstream error.
NodeAPI Terminal error could not be written warn requestId, optimizationRunId, internal write error Captures the rare best-effort response-write failure.
petrinaut-opt API Optimizer capacity reached (capacity_rejected) warning request_id, active/max counts Explains the optimizer-service 429 rejection.
petrinaut-opt API CLI/model initialization failed (initialization_failed) error request_id, run_id, error_type Locates pre-stream startup failures without logging the raw exception text.
Optimizer Study started (study_started) info request_id, run_id, trials Marks the start of Optuna execution.
Optimizer Study completed (study_completed) info request_id, run_id, trials Marks successful Optuna completion.
Optimizer Study failed (study_failed) warning request_id, run_id Marks terminal study failure without copying user-authored error text.
Optimizer Client disconnected (client_disconnected) info request_id, run_id Shows why an in-progress study was cancelled.
Optimizer Worker did not stop (worker_join_timeout) error request_id, run_id Flags a cleanup failure that left the worker running after CLI shutdown.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

🛡 What tests cover this?

  • @apps/petrinaut-opt unit suite — 53 passed
  • Focused Hash API handler/setup tests — 22 passed
  • Focused Hash API ESLint — passed
  • @hashintel/petrinaut-cli unit suite — 33 passed; typecheck and ESLint passed
  • Focused Petrinaut website lint — passed
  • @local/petrinaut-optimizer-client build — passed
  • Three-way merge simulation with the SRE-810 OTEL branch — clean, no conflicts

The fresh worktree does not contain the generated Petrinaut/design-system artifacts required for a package-wide website typecheck; CI is the authoritative full-package validation.

🐾 Remaining work

  1. In SRE-810, replace HASH_OTLP_ENDPOINT with standard OTEL configuration (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME, etc.).
  2. Land the OTEL provider/exporter and FastAPI/Optuna span work after or on top of this PR.
  3. On staging, reproduce the network error and use the NodeAPI terminal record (durationMs, outcome, optimizationRunId) to identify the failing boundary.

How the loggers work

NodeAPI uses the existing application logger and passes requestId with each record. This avoids constructing a new Winston logger and transport set per request. It logs only operational boundaries: local capacity, upstream stream-open, disconnect, and terminal success/failure.

Python uses the named stdlib loggers pn_api and pn_optimize, attaching stable fields such as event, request_id, and run_id through extra. It does not install a handler or exporter here. The complementary OTEL setup can export those records and add active span context without competing configuration.

The CLI cannot log to stdout because stdout carries protocol responses. Its stderr pipe is drained and discarded solely to prevent backpressure; this PR does not turn CLI output into service logs.

@kube kube self-assigned this Jul 19, 2026
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jul 23, 2026 3:10pm
petrinaut Ready Ready Preview Jul 23, 2026 3:10pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 23, 2026 3:10pm

@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/infra Relates to version control, CI, CD or IaC (area) area/apps > hash-api Affects the HASH API (app) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team area/tests New or updated tests area/apps labels Jul 19, 2026
@kube
kube marked this pull request as ready for review July 19, 2026 04:18
@kube
kube requested a review from YannisZa July 19, 2026 04:18
@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches the live optimization proxy path, streaming error handling, and new OTLP bootstrap; logging changes are designed to avoid leaking user content but the handler behavior on failures/disconnects is security- and ops-sensitive.

Overview
Adds end-to-end correlation for Petrinaut optimization: NodeAPI forwards x-hash-request-id, the optimizer client returns optimizationRunId from X-Optimization-Run-ID, and both sides emit bounded lifecycle logs (requestId / request_id, run_id) without manifests or user-authored error text.

NodeAPI logs capacity rejections, stream open, terminal outcomes (completed, upstream-error, client-disconnected, timeouts), and exposes X-Optimization-Run-ID to clients. Stream handling now tracks domain terminal outcome so disconnect/backpressure cannot mis-report success after an upstream error.

petrinaut-opt adds OpenTelemetry (telemetry.py: OTLP traces/metrics/logs when OTEL_EXPORTER_OTLP_ENDPOINT is set), FastAPI instrumentation, optimization.study / optimization.trial spans with context on worker threads, structured API/optimizer lifecycle logs, and flush on shutdown.

Tests and README document correlation, privacy constraints, and observability setup.

Reviewed by Cursor Bugbot for commit 4de6125. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/hash-api/src/petrinaut-optimizer/create-petrinaut-optimization-handler.ts Outdated
YannisZa
YannisZa previously approved these changes Jul 19, 2026
kube added 2 commits July 19, 2026 23:02
…ization pipeline

One optimization is now traceable end to end: NodeAPI's x-hash-request-id
is forwarded upstream, the optimizer's run_id comes back as
X-Optimization-Run-ID and is logged on both sides, and the CLI receives
the run id at spawn as PETRINAUT_CORRELATION_ID.

- petrinaut-optimizer-client: openPetrinautOptimizationStream accepts a
  requestId (sent as x-hash-request-id) and returns { events,
  optimizationRunId } read from the X-Optimization-Run-ID header.
- hash-api: request-scoped child logger (Apollo pattern); logs request
  acceptance, validation failures (issue counts and schema paths only),
  capacity rejections, stream opening, and a terminal outcome with
  durationMs (completed / timeout kind / upstream-error /
  client-disconnected — disconnects no longer return silently).
- petrinaut-opt: stdlib JSON-lines logging to stdout (logging_config.py,
  HASH_PETRINAUT_OPT_LOG_LEVEL, idempotent setup); inbound
  x-hash-request-id and traceparent headers threaded into every study
  lifecycle log (admission, capacity rejection, initialization,
  study start/completion, per-trial outcomes, wall-clock ceiling,
  disconnect, worker join timeout, CLI termination path, slot release).
  CLI stderr is no longer discarded: each line is forwarded to a pn_cli
  logger, truncated with a bounded pending buffer.
- petrinaut-cli: minimal JSON stderr diagnostics after the unchanged
  ready handshake (bootstrap_completed, per-request id/method/durationMs/
  outcome, stdin_eof), correlation id injected via cli.ts.

Logs never include manifests, user-authored code, or raw request bodies;
docs updated (README logging section, CLI stderr contract, threat model).
- Cap forwarded CLI stderr at 1000 lines/run (drain the rest) so a chatty
  or hostile CLI cannot flood the log pipeline; seed the drain thread with
  bytes read past the bootstrap handshake so an early CLI diagnostic is
  forwarded rather than dropped.
- Truncate the per-trial pruned-error field and stop interpolating the raw
  CLI error into the log message; correct the docstring.
- Log an error type and coarse category instead of the raw initialization
  error string, which can embed a user identifier or expression error; the
  full message still reaches the requester in the 500 detail.
- Cap validation issue-path segments before they reach the 400 response or
  the log, since manifest keys are user-controlled and unbounded.
- Capture the optimizer run id from a failed response so a pre-stream
  upstream failure still correlates end to end.
- Charset-validate the traceparent trace id instead of int(..., 16), which
  accepted 0x-prefixed, underscored, and signed values.
- Document the aggregate stderr cap and header validation in the threat
  model.
@kube
kube force-pushed the cf/fe-1221-petrinaut-optimization-logging branch from e481051 to f813baf Compare July 19, 2026 21:02
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 22, 2026 23:45 Inactive
Comment thread apps/petrinaut-opt/src/telemetry.py
@YannisZa

Copy link
Copy Markdown
Contributor

@kube do you want to change the description of the PR to mention that it now includes yz/sre-810-otlp-for-petrinaut-opt?

@kube kube changed the title FE-1221: Add structured correlated logging across the Petrinaut optimization pipeline FE-1221, SRE-810: Add structured correlated logging across the Petrinaut optimization pipeline Jul 23, 2026
Comment thread apps/petrinaut-opt/src/telemetry.py

@claude claude 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.

Reviewed with a focus on the OTel setup now included in this PR (SRE-810) alongside the correlated logging. Two blocking issues: (1) the new INFO-level lifecycle records are never emitted in production — the root logger stays at WARNING under python -m uvicorn, so the OTLP LoggingHandler receives nothing below WARNING (tests mask this via caplog.at_level); (2) the Docker HEALTHCHECK's GET /status probe is instrumented and produces a span every 30s whose name won't match the gateway's health-check drop rules, spamming Tempo (SRE-844-class noise) — excluded_urls fixes it at the source. One should-fix: the pruned-trial warning ships raw CLI stderr (potentially user-authored content) to Loki once the log bridge is live, contradicting the PR's own redaction rule. One non-blocking suggestion: constructing the OTLP exporters with explicit endpoint=/insecure= bypasses the standard per-signal/insecure env vars the SDK would honour on its own.

What I checked and found sound: provider wiring order (tracer/meter/logger providers set globally before instrument_app, resource/service name matches the infra-provisioned OTEL_SERVICE_NAME default, grpc default matches OTEL_EXPORTER_OTLP_PROTOCOL=grpc); graceful-shutdown flush via the lifespan; the study/trial span hierarchy — the explicit contextvars re-attach on the Optuna worker thread correctly parents optimization.trial under optimization.study under the request span, and FastAPI's instrumentation will extract an incoming W3C traceparent from the Node API when the caller injects one; pruned trials correctly avoid ERROR span status; the study span is reliably ended in the stream generators' finally; SSE streaming interacts fine with the instrumentation (long-lived request spans export on completion). I didn't re-raise points from existing threads (e.g. the open Bugbot note that shutdown_telemetry leaves _configured = True, which is worth addressing for the multi-lifespan/test case).

Comment thread apps/petrinaut-opt/src/telemetry.py Outdated
Comment thread apps/petrinaut-opt/src/telemetry.py Outdated
Comment thread apps/petrinaut-opt/src/petrinaut_optimizer.py
Comment thread apps/petrinaut-opt/src/telemetry.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9b3110e. Configure here.

Comment thread apps/petrinaut-opt/src/telemetry.py

@claude claude 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.

Toolkit pass (pr-review-toolkit review-pr orchestration) supplementing the earlier Claude review at ee91b5f31 — reviewed at head 9b3110e89.

Dimensions run: code-reviewer, pr-test-analyzer, comment-analyzer, silent-failure-hunter, type-design-analyzer (code-simplifier skipped per command rules). Findings already raised by the existing Claude, Bugbot, and Graphite threads were deduplicated away; only the two README accuracy issues below are new.

Re-confirmed at this head, without re-raising: the four open comments from the earlier Claude review still apply, and Bugbot's open finding (shut-down TracerProvider left globally registered when bootstrap fails after trace.set_tracer_provider) is real — global registration happens before instrument_app can fail, and the OTEL API offers no un-set, so registration would need to move after the last fallible step. Notably solid in this PR: the terminal-outcome commit semantics in forwardOptimizationEvents, the lifecycle/timeout listener cleanup, the partial-bootstrap rollback test, and test coverage of every new lifecycle event on both sides of the service boundary.

Comment thread apps/petrinaut-opt/README.md
Comment thread apps/petrinaut-opt/README.md Outdated
YannisZa
YannisZa previously approved these changes Jul 23, 2026
@kube
kube added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit 8daf815 Jul 23, 2026
53 checks passed
@kube
kube deleted the cf/fe-1221-petrinaut-optimization-logging branch July 23, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) area/apps area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

2 participants