Parallax Observability Findings
Status: Findings from live Parallax data, not mock data. Collected on July 3, 2026 from 127.0.0.1:4000, Parallax CLI, Parallax SQL, and the current jackin❯ codebase.
Scope
This page records what is wrong with the current logs/traces/issues shape as seen by an OpenTelemetry-style backend. It is intentionally diagnostic first. Implementation should start with logs, because logs are the highest-volume and most visibly unreadable signal.
Important correction: the goal is not to reduce observability. The goal is to increase trustworthy coverage while reducing unusable payload. jackin❯ needs more complete tracing/logging/metrics around real workflows, slow paths, external calls, process execution, and errors. The current problem is that too much exported signal is unstructured firehose while many important operations are missing typed outcomes, correlation, and duration.
Decision rule for follow-up work: fix what is correct to fix. Do not preserve known-wrong behavior because it is "low value", "marginal", "an edge case", costly, large, or similar to what another system does. The only valid reason to stop short is a proven tool, model, or project capability limit.
Bug rule for follow-up work: every telemetry bug is evidence that the architecture allowed that bug class. Before fixing an individual symptom, identify the enabling structure and prefer the structural fix that prevents the class from recurring. Use symptom patches only when the root fix is proven infeasible or belongs in a separate explicitly named change.
Inputs used:
- Parallax UI pages supplied by the operator: Issues list, Issue detail, Trace detail.
- Additional operator screenshots/pastes from July 3, 2026:
- Runs list with 52 visible runs.
- Run detail for
18bea0c913570db0. - Logs excerpt with
[jackin-capsule debug]PTY byte dumps. - BuildKit trace detail for
2c2c18b26eee1bb9b8c4692119a5b957.
parallax logs --since 30d --limit 80parallax traces --since 30d --limit 80parallax run listparallax issue listparallax doctorparallax sql "SHOW TABLES"parallax sql "DESC opentelemetry_logs"parallax sql "DESC opentelemetry_traces"- Aggregate SQL over
opentelemetry_logsandopentelemetry_traces. - Code inspection of diagnostics/logging/tracing paths.
- OpenTelemetry primary documentation:
- Rust tracing documentation:
Executive Findings
-
Logs are console-formatted before export. Prefixes like
[jackin debug docker]and[jackin-capsule debug]are embedded inbody, not exposed as structured fields. That makes Parallax/Kibana-style filtering depend on text search instead of dimensions such asjackin.log.category,jackin.component,jackin.operation,container.name, orstage. -
Debug firehose is exported as first-class logs. The Parallax store had
968,593DEBUGlogs across34runs, versus8,977INFO,4WARN, and1ERROR. This overwhelms useful runtime events.parallax doctorshowed the same cost at rest:logs.ndjsonspool was1.65 GiB, while traces spool was273.1 MiBand metrics spool was118.6 MiB. -
Some records include sensitive or excessive payloads. Docker inspect output exported full container JSON, including environment arrays with token-shaped values and very large mount/config payloads. Telemetry must redact and summarize before export.
-
Error visibility is broken. SQL found
1,686log bodies mentioningerror,failed, orpanic, but only1OTelERRORlog and only2STATUS_CODE_ERRORspans. Both error spans came frombuildx, not jackin❯. Most jackin❯ failures are plainINFO/DEBUGbodies or JSONL diagnostics events, so Parallax cannot reliably group them as issues.Updated query including
eoffound1,757error-like log bodies:1,594DEBUG,162INFO, and1ERROR. Example from run detail:attach client: socket read failed: early eofisINFO, notERROR. -
Issue grouping fingerprints runtime noise. The Issues page showed 14 open issues, mostly the same capsule attach failure split by container name, user id, run id, branch/path, and command text. Each had one event. This defeats issue grouping.
-
Issue detail bodies are too large and unstructured. A single attach failure page inlined a 40-line capsule debug tail with timestamps, terminal byte dumps, frame metrics, paths, and repeated prefixes into one paragraph. The actual failure is buried.
-
Trace shape is inconsistent. There are many traces with one span or almost no correlation, and other traces with hundreds or thousands of tiny internal spans. Example:
usage:refresh_accountshad588,1005, and similar span counts; top aggregate showed109,579emit_insnspans across1,358traces. The latest run detail showed an18-trace launch whereusage:refresh_accountshad440spans, whilecapsule:tab,capsule:session, andprewarm_auth_for_agentswere single-span traces. -
Launch-stage spans are not semantically distinguishable in the UI. The Trace detail page showed 12 waterfall rows all named
launch_stage; the meaningfulstage=derived imagevalue appears only in the attributes panel. Operators cannot scan the waterfall. -
Logs and traces are not consistently correlated. The sample Trace detail page showed
Logs: 0for a launch trace even when run-level logs existed. Logs often carry run id but not the correct active span/trace context. -
Run id exists and is useful, but it is not enough.
parallax.run.idis present on log rows and run pages work, but cross-run grouping, per-run summary, trace linkage, and error grouping need stable semantic fields. -
High-frequency UI and terminal internals dominate run logs. SQL found
242,689PTY/render debug rows (session feed_pty bytes,send-bytes,frame-geom,pane scroll frame) and21,611cockpit mouse rows. The run page for18bea0c913570db0showed repeated mouse move rows as newest logs, so useful lifecycle events are pushed below interaction noise. -
Some run and trace evidence bundles are too raw for operator use. The run bundle includes Docker inspect JSON, full generated Dockerfile text, raw image layer arrays, command URLs, host paths, and debug lines. That data can be useful as an artifact, but it should not be the default log body or issue evidence.
-
Coverage is incomplete in the places most needed for jackin❯ development. Slow external calls, subprocesses, Docker lifecycle steps, agent binary resolution/downloads, credential/provider resolution, capsule attach/session lifecycle, and cleanup need typed spans/logs/metrics. Some of these are visible today as plain text (
held the foreground launch path,Building Docker image, raw URLs), but not as a reliable operation graph. -
Parallax should support cross-trace navigation as a first-class feature. OpenTelemetry has parent/child spans within one trace and standard
Span Links that can reference spans in the same or another trace. It does not define a "child trace id" hierarchy as a separate native concept. Parallax can addparallax.parent_trace_id,parallax.trace.role, or similar backend navigation attributes, but should also ingest and render standard OTel span links. -
Observability needs layered verbosity, not one global firehose. Today
--debug/JACKIN_DEBUG=1is a binary switch that enables too many categories atDEBUG. jackin❯ should replace that with explicit levels (info,debug,trace) and category filters so normal development gets rich workflow visibility, while byte/frame/keypress internals only appear when explicitly requested. -
OTLP mode must not reference local diagnostics files.
RunDiagnosticsalready treats JSONL files as a fallback sink and does not persist them by default when OTLP export is active. But some UI/capsule surfaces still imply a file location. In OTLP mode the canonical reference is the run id and backend query, not~/.jackin/data/diagnostics/runs/<run>.jsonl.
OpenTelemetry Subtrace Finding
The operator hypothesis is mostly correct: OpenTelemetry does not model "subtrace parent-child" as a special trace-to-trace hierarchy. The standard model is:
- A trace is a tree/DAG of spans sharing one
trace_id. - Each span has zero or one parent span and zero or more child spans inside that trace.
- A root span starts a new trace and gets a new
trace_id. - A span can include links to other spans, including spans in another trace.
Implication:
- If jackin❯ wants one giant launch view, use one trace with parent/child spans.
- If jackin❯ wants readable subtraces, create separate traces and connect them with OTel span links.
- Parallax should render those span links as "parent workflow", "linked subtrace", and "back to parent" navigation.
- For backend-native UX, Parallax may additionally store:
parallax.parent_trace_idparallax.parent_span_idparallax.link.kind=child_workflow|external_subtrace|continuationparallax.workflow.idparallax.workflow.parent_id
Recommendation: do both. Emit standard OTel links for portability, and let Parallax derive or store Parallax-specific relation fields for better UI and CLI navigation.
Instrumentation Coverage Findings
The current system has some good primitives:
RunDiagnostics::stagetracks launch stage start/done/fail/skipped events and duration summaries.active_timing_started/active_timing_donecan record nested timings under a stage.ShellRunnercentralizes most host subprocess execution.crates/jackin-docker/src/net.rscentralizes some HTTP text fetch and parallel download paths.- Capsule OTLP initialization stamps
session.idand can link capsule session start to hosttraceparent.
The problem is uneven application and weak exported semantics.
Required Coverage Map
Every long or failure-prone operation should have:
- one span with stable name,
- concise lifecycle logs,
- typed outcome (
success,failure,timeout,cancelled,skipped,cache_hit,cache_miss), - duration metric,
- low-cardinality attributes,
- error attributes and span status on failure,
- redacted artifact links for large payloads.
Priority workflows:
| workflow | needed spans | needed logs | needed metrics |
|---|---|---|---|
| CLI/run lifecycle | run.start, run.finish, run.cancel | command summary, exit status | run duration, exit count |
| role resolution | role.resolve, role.repo_refresh | source, trust result | duration, cache hit/miss |
| credential resolution | credentials.resolve, credentials.operator_env, credentials.provider_lookup | key names only, provider, outcome | duration by provider, missing count |
| agent binary setup | agent_binary.resolve, agent_binary.download, agent_binary.verify | agent, version, source, cache | bytes, duration, cache hit/miss, failures |
| HTTP metadata/API calls | http.client.request or external.api.call | method, host, route class, status | latency, status family, retry count |
| downloads | download.prefetch, download.transfer, download.verify | host, artifact kind, version | bytes, throughput, duration, retry count |
| Docker lifecycle | docker.container.inspect, docker.container.create, docker.container.start, docker.network.create, docker.volume.create | object/action/state | duration, count, failure count |
| image build | image.build.plan, image.build_context.publish, image.buildx.solve | build reason, image, cache decision | duration, context bytes, steps, cache hits |
| attach | capsule.attach, capsule.handshake, attach.proxy | container, tty mode, outcome | duration, failures by type |
| capsule session | capsule.session.start, agent.session.start, agent.session.exit | agent, command, exit | session duration, exit code count |
| terminal/render hot path | no default per-event spans | sampled/debug artifact only | bytes, frames, render duration, invalidations |
| cleanup | cleanup.container, cleanup.network, cleanup.volume, cleanup.artifacts | preserved/removed, reason | duration, failures |
Current Gaps From Code Inspection
emit_debug_lineformats[jackin debug {category}]before backend export, so structured categories are lost unless a backend parses body text.RunDiagnostics::stagecreates repeatedlaunch_stagespans, which records timing but loses scannable operation names.active_timing_started/donerecords nested timings as JSONL events and summary maps, but they are not real child spans, so Parallax cannot show them in the waterfall.ShellRunnercaptures command execution centrally, but exported telemetry is command text and first-line stdout/stderr, not stableprocess.*/command.*attributes with duration, exit code, and error type.- HTTP fetch/download helpers expose slow external work, but current live Parallax data shows URLs as plain
INFObodies rather than typed client spans/metrics. - Capsule debug logs emit rich terminal internals, but the important state machine transitions are not separated from byte/frame firehose.
Structural direction:
- Add a typed operation API around existing diagnostics:
operation_span(name, attrs)operation_log(event_name, body, attrs)operation_error(error_type, err, attrs)operation_metric(name, value, attrs)
- Use it first in shared choke points:
ShellRunner- HTTP client/download helpers
- Docker client lifecycle helpers
- launch stage/timing API
- capsule attach/session state machine
- Keep
cdebug!for local deep-debug, but do not use it as the main observability contract.
Layered Verbosity Model
Rust tracing distinguishes spans from events: spans represent work with duration and causality; events represent points in time. OpenTelemetry logs define severity ranges: TRACE is fine-grained debug normally disabled, DEBUG is debugging information, INFO is that something happened, WARN is important but not an error, ERROR means something went wrong, and FATAL is crash/shutdown level. jackin❯ should map to that model directly.
CLI And Environment Contract
Current state:
--debug/JACKIN_DEBUG=1is boolean.- Host OTLP filter chooses
debugwhen debug is true, otherwiseinfo. - Capsule uses
JACKIN_DEBUGto pickdebugversusinfo. - Many deep internals are also controlled by this same debug boolean.
Desired state:
jackin --telemetry-level info
jackin --telemetry-level debug
jackin --telemetry-level trace
jackin --telemetry-category docker,launch,credentials
jackin --telemetry-category terminal,render,inputEnvironment equivalents:
JACKIN_TELEMETRY_LEVEL=info|debug|trace
JACKIN_TELEMETRY_CATEGORIES=docker,launch,credentials
JACKIN_TELEMETRY_INTERNAL=0|1Rules:
- Default is
info. - Do not keep compatibility aliases for changed telemetry configuration.
- Remove
--debug/JACKIN_DEBUGas telemetry controls when the new model lands; use only--telemetry-level/JACKIN_TELEMETRY_LEVEL. - If a separate non-telemetry debug UI is still needed, give it a different explicit name so it cannot be confused with telemetry export level.
traceis the explicit deep-firehose mode.- Categories narrow or expand noisy domains without changing unrelated domains.
- Each backend layer can have its own filter:
- console: compact operator-facing info/warn/error,
- local diagnostics artifact: debug/trace when requested,
- OTLP: structured logs/spans/metrics at selected level,
- Parallax bundle: summarized evidence plus artifact links.
Level Taxonomy For jackin❯
| level | use for | examples | default export |
|---|---|---|---|
ERROR | failed operation that affects the run or user workflow | attach failed, image build failed, credentials unavailable, command exit nonzero | always |
WARN | degraded/recovered/expected handled issue that may explain behavior | retry exhausted then fallback, cleanup partial failure, exporter unavailable, missing optional tool | always |
INFO | operator-meaningful lifecycle and decisions | run started/finished, launch plan selected, image build started/done, container started, agent session exited | default |
DEBUG | developer diagnostics with bounded volume | cache decision details, command summary, Docker state before/after, retry attempt summary, selected config source | --debug |
TRACE | per-event firehose and byte/frame internals | PTY byte dumps, render frame geometry, mouse move coordinates, per-keypress dispatch, raw protocol frames | --telemetry-level trace and/or category opt-in |
What Moves From Debug To Trace
These should not be DEBUG in OTLP by default because they are high frequency and rarely useful in Parallax search:
session feed_pty bytessend-bytesrender: ratatui-framepane scroll frameframe-geomcockpit-dialog-mouse kind=Moved- per-keypress parser transitions,
- raw protocol frames,
- repeated terminal invalidation rows.
Default handling:
- summarize with metrics,
- sample at
DEBUGonly when useful, - emit full rows only at
TRACEor with category opt-in:JACKIN_TELEMETRY_CATEGORIES=terminal,render,input
What Stays Debug
DEBUG should answer "why did jackin❯ choose this path?" without requiring raw bytes:
- cache hit/miss reasons,
- selected launch plan and rejected alternatives,
- Docker object state summaries,
- command argv summaries with redaction,
- retry attempt summary,
- config source resolution,
- feature flag decisions,
- provider/version selection.
What Must Be Info
INFO should be enough for the default Parallax run page to explain normal startup:
run.startedrun.finishedlaunch.plan.selectedstage.started/stage.finishedfor major stages,credentials.resolvedagent_binary.resolvedimage.build.started/image.build.finishedcontainer.startedcapsule.attach.started/capsule.attach.finishedagent.session.started/agent.session.finished
These events should be low volume and structured. They should carry event.name, event.outcome, jackin.stage, jackin.operation, duration_ms, and run/trace/span context.
What Must Be Warn Or Error
Do not bury failures in INFO body text.
ERROR: operation failed and the current command/run failed or a user-visible capability failed.WARN: operation failed but was handled, retried, skipped by design, or fell back successfully.DEBUG: exception/noise that does not indicate a problem, such as expected client-side cancellation.
OpenTelemetry exception guidance says severity reflects expected impact, not merely the presence of an exception. For jackin❯ this means early eof during expected terminal shutdown should be typed as expected close, while early eof during attach handshake should be ERROR or WARN depending on whether attach recovered.
Span/Event/Log Split
Use spans for duration and hierarchy:
- external command,
- HTTP call,
- Docker call,
- image build,
- attach handshake,
- credential lookup,
- agent binary download,
- cleanup operation.
Use span events for notable points inside an active span:
- retry attempt,
- cache hit/miss,
- selected fallback,
- progress milestone,
- linked artifact created.
Use logs for human-readable facts that operators search:
- final failure summary,
- normal lifecycle milestone,
- unexpected recovered condition,
- exporter unavailable notice.
Use metrics for high-frequency counters/gauges:
- terminal bytes,
- render frame counts,
- mouse event counts,
- process CPU/memory,
- command duration histogram,
- HTTP latency/status counts.
Attribute And Tag Rules
Every exported log/span should include stable facets:
event.nameevent.outcomejackin.component=host|capsule|consolejackin.operationjackin.stagejackin.categoryparallax.run.idsession.idwhen inside capsule/sessionerror.typewhen failedartifact.id/artifact.path_redactedfor large local evidence
Avoid high-cardinality fields as grouping tags:
- full command strings,
- full URLs with query,
- full temp paths,
- container ids as fingerprints,
- raw stdout/stderr,
- raw Docker inspect JSON,
- raw terminal bytes.
Rust Implementation Guidance
- Use
tracingspans for work units and events for point facts, matching the crate model. - Use
EnvFilterper layer so console, local file, OTLP logs, and OTLP spans can have different filters. - Keep target/module filters for dependency noise, but do not disable jackin❯ operation spans needed for development.
- Prefer explicit fields over formatted strings:
tracing::info!(
target: "jackin::launch",
event.name = "launch.plan.selected",
jackin.operation = "launch.plan",
jackin.stage = "plan",
event.outcome = "success",
plan = %plan_name,
"launch plan selected"
);Bad default OTLP body:
[jackin debug docker] inspect container jk-abc -> runningGood structured log:
body="container inspected"
severity=DEBUG
event.name=docker.container.inspect
jackin.category=docker
docker.object=container
container.name=jk-abc
container.state=runningParallax UX Requirements
- Logs page should show severity filter with OTel severity ranges, not only raw
severity_text. - Logs page should show columns for
event.name,jackin.operation,jackin.stage,event.outcome,error.type. - Run page should default to
INFO+lifecycle timeline and hideDEBUG/TRACEbehind toggles. - Trace page should let operator toggle span events/logs by severity.
- Bundle should include:
INFO+timeline,- warnings/errors,
- top slow spans,
- linked subtraces,
- artifact links,
- optional
DEBUG/TRACEsections only when requested.
OTLP Sink Contract
When OpenTelemetry export is active, logs, traces, and metrics are sent over OTLP to Parallax or another backend. They are not stored in jackin❯ JSONL diagnostics files by default.
Current code mostly intends this:
RunDiagnostics::startsetspersist = !otlp_active || diagnostics_file_forced().JACKIN_DIAGNOSTICS_FILE=1is the explicit opt-in to write files and OTLP at the same time.write_command_outputskips sidecar creation when the run file writer is disabled.RunDiagnostics::path()is a "would-be path" unlesspersists()is true.
Required product contract:
- If OTLP export is active and
JACKIN_DIAGNOSTICS_FILEis unset, do not display local diagnostics paths. - Do not print "see
<path>.jsonl" or "Reveal diagnostics" for a file that does not exist. - Do not pass
JACKIN_RUN_DIAGNOSTICS_PATHinto the capsule unless a file is actually persisted. - Surfaces should display:
Telemetry run id: <run_id>View in Parallax: parallax run <run_id>Logs: parallax logs --run <run_id>Traces: parallax traces --run <run_id>Bundle: parallax run bundle <run_id>
- If the backend is not Parallax, show backend-neutral text:
Use parallax.run.id=<run_id> in your OpenTelemetry backend.
Concrete current issue:
crates/jackin-capsule/src/container_context.rssynthesizes~/.jackin/data/diagnostics/runs/{run_id}.jsonlwhenJACKIN_RUN_DIAGNOSTICS_PATHis absent.- In OTLP mode this is misleading because the host intentionally did not create the file.
- The fallback should be changed to "backend only" reference when no diagnostics path is provided:
- display run id,
- no
file://href, - no "Reveal diagnostics" row.
Desired UI rows in OTLP mode:
| label | value |
|---|---|
| Run ID | <run_id> |
| Telemetry | Parallax / OpenTelemetry |
| Query | parallax run <run_id> |
Desired UI rows only when file persistence is active:
| label | value |
|---|---|
| Diagnostics file | <actual path from JACKIN_RUN_DIAGNOSTICS_PATH> |
| Reveal diagnostics | file link |
Acceptance checks:
- Start with OTLP enabled and no
JACKIN_DIAGNOSTICS_FILE. - Confirm no
diagnostics/runs/<run>.jsonlfile is created. - Confirm capsule/container info shows run id but no file path and no reveal-file action.
- Confirm any error/help text points to
parallax run <run_id>orparallax logs --run <run_id>. - Start with
JACKIN_DIAGNOSTICS_FILE=1. - Confirm file path is shown only in that mode and the file exists.
UI Findings
Issues List
Observed page: Issues.
Problems:
- Titles are raw technical payloads, not normalized symptoms. Examples included full
docker exec ... /jackin/runtime/jackin-capsulecommand lines and BuildKit shell commands. - The same attach failure appeared as many separate issues because dynamic values are part of the title/fingerprint:
jk-qfrehkbv-holla-thearchitectjk-p45np22c-holla-thearchitectjk-xb9cw69q-jackin-thearchitect--user 501:0,--user 501:20, or no--user
- Tags shown in rows were low-value (
detail:<none>,jackin_jsonl:true) instead of operational facets (operation=attach,component=host,failure.kind=attach_error,agent=claude,runtime=docker). - All repeated attach failures had
EVENTS=1, so the list reads like many unique incidents instead of one recurring incident. - KPI cards duplicate visible counts (
14 loaded,14 open,14 events) without explaining root causes or dominant failure classes.
Desired shape:
- Issue title:
Capsule attach failed - Fingerprint fields:
service.name,jackin.component,error.type,jackin.operation, normalized failure code. - Dynamic data as attributes only:
container.name,run.id,user.uid, command argv, log artifact path. - Row facets:
operation=attach,runtime=docker,agent=claude,stage=attach,run_count=N,last_run=<id>.
Issue Detail
Observed page: log_error: capsule attach failed ....
Problems:
- H1 is a full variable command string, so the page title itself is unreadable.
- Latest event body contains:
- actual failure line,
- capsule log path,
last 40 capsule log lines,- repeated
[jackin-capsule debug]prefixes, - terminal byte dumps,
- render/frame telemetry,
- paths and dynamic branch/container names.
- Tags include
detail=<none>andjackin_jsonl=true, which are implementation markers, not triage data. - Occurrence row repeats the whole giant body; no separation of summary, cause, evidence, and artifact links.
- The page links one trace, but there is no structured "what failed, where, why, what run, what stage, what command" section.
Desired shape:
- Summary:
Capsule attach failed while connecting host to capsule socket. - Cause fields:
error.type=attach_errorjackin.operation=capsule.attachjackin.stage=attachcommand.name=dockerprocess.exit_codeorsignalwhen knowncontainer.namecontainer.state
- Evidence:
- first failure line,
- last meaningful capsule log events after filtering debug firehose,
- link to full artifact/run bundle,
- redacted command argv.
- Raw debug tail should be a downloadable/expandable artifact, not the issue body.
Trace Detail
Observed page: launch_stage trace.
Problems:
- Waterfall rows all have the same visible name:
launch_stage. - The meaningful stage name (
derived image) is hidden under attributes. - The trace duration card says
123118.4ms, but the selected row has99849.528ms; the UI does not explain which stage consumed the run. Logs: 0on a launch trace conflicts with run-level logs for the same run. This means logs were not emitted inside the span context or were exported without the active trace/span linkage.- Many tiny rows with
0.02ms/0.06msare visually present but operationally meaningless.
Desired shape:
- Span name should be operation-specific, such as:
launch.planlaunch.prepare_workspacelaunch.build_derived_imagelaunch.create_containerlaunch.attach_capsule
- Attributes should carry facets:
jackin.stage,jackin.role,container.name,image.name,cache.hit. - Waterfall label should render
operation + stage, not just the generic operation. - Logs emitted during a launch stage must inherit the active span context.
Runs List
Observed page: Runs.
Problems:
- The page had
52visible runs:48finished wrapper runs,2observed-only telemetry runs, and10failed exits. - Most rows had the same command text:
jackin --debug. That makes the table useful for chronology but not for diagnosis. - Failed runs only show
exit 1; the row does not show top issue, failing stage, last error, or dominant error type. - Observed-only rows show span/log counts such as
14387 span(s) · 0 log(s), which is strong evidence of uncorrelated traces without enough run context. - The status summary says failures exist, but the first-screen list does not explain what failed or whether failures share a cause.
Desired shape:
- Add columns/facets for
workflow,role,agent,workspace,top_error.type,top_issue, andfailed_stage. - Show
exit 1 · capsule attach failedinstead of onlyexit 1. - Group repeated
jackin --debugrows by role/workflow where useful, while preserving exact run ids. - Treat observed-only runs with many spans and zero logs as telemetry-health warnings.
Run Detail
Observed page: run 18bea0c913570db0.
Problems:
- Header showed
finished,exit 0,18 trace(s),0 error(s), and200 log(s), but logs includedattach client: socket read failed: early eof. - The newest logs were repeated mouse movement/debug rows, both prefixed and unprefixed:
[jackin debug cockpit-dialog-mouse] kind=Moved ...kind=Moved ...
- Trace list mixed jackin❯ workflow traces, BuildKit traces, provider probes, capsule session traces, and one-span helper traces without a hierarchy.
- The evidence bundle inlined raw Docker inspect output and generated Dockerfile content. This makes the run bundle hard to skim and creates redaction risk.
No grouped issues inside this runis technically true for this sample, but the page still contains failure-like signals. The UI cannot distinguish expected EOF/noise from actionable errors because jackin❯ does not send typed outcomes.
Desired shape:
- Run summary should show lifecycle phases with status:
role.resolvecredentials.resolveagent_binaries.prepareimage.buildcontainer.createcapsule.attachagent.session
- Expected terminal/session shutdown should be
event.outcome=success|expected_close, not body text containingfailed/eof. - Debug interaction rows should be sampled, counted, or moved to metrics/artifacts.
- Raw large artifacts should be linked from the bundle, with summarized structured rows in the default view.
Build Trace Detail
Observed page: trace 2c2c18b26eee1bb9b8c4692119a5b957, root build /var/folders/.../.tmpJxRImQ/context.
Problems:
- The trace is a useful BuildKit subtrace, but it is presented as a peer trace to jackin❯ launch instead of a linked child of the launch/image-build workflow.
- The root name includes a host temp path, which is dynamic and noisy.
Logs: 0despite the same run having image build logs such asBuilding Docker imageandderived image build source selected.- Waterfall rows are BuildKit RPC names (
moby.buildkit.v1.Control/Status,LLBBridge/Solve) with no jackin❯ semantic wrapper likelaunch.build_derived_image. - The trace is dominated by two long BuildKit spans (
Control/Solve,Control/Status) but the page does not surface the jackin❯ reason for the build: cache miss, capsule version change, recipe hash change, rebuild flag, etc.
Desired shape:
- Parent launch trace span:
launch.build_derived_image. - Linked BuildKit subtrace:
linked.trace_id=<buildx trace>jackin.parent_trace_id=<launch trace>jackin.operation=image.buildbuild.reason=capsule_version_changedimage.name,image.recipe.version,image.recipe.hash
- Root name should be stable:
buildx.build_image, with temp context path as redacted attribute or local artifact reference. - Logs emitted by jackin❯ around the build should correlate to the parent span and, when possible, to the BuildKit linked trace.
Raw Data Findings
Log Volume
Query:
SELECT severity_text, COUNT(*) AS logs, COUNT(DISTINCT trace_id) AS traces,
COUNT(DISTINCT `parallax.run.id`) AS runs
FROM opentelemetry_logs
GROUP BY severity_text
ORDER BY logs DESC;Result:
| severity | logs | traces | runs |
|---|---|---|---|
| DEBUG | 968,593 | 2,815 | 34 |
| INFO | 8,977 | 137 | 34 |
| WARN | 4 | 4 | 4 |
| ERROR | 1 | 1 | 1 |
Interpretation: --debug turns telemetry into a firehose. Backend UX becomes dominated by debug internals, not user-relevant lifecycle and failure events.
Local Storage Impact
parallax doctor reported:
| data | size |
|---|---|
| data dir | 2.91 GiB |
| spool logs.ndjson | 1.65 GiB |
| spool traces.ndjson | 273.1 MiB |
| spool metrics.ndjson | 118.6 MiB |
| engine data | 431.5 MiB |
Interpretation: logs are not only unreadable in the UI; they dominate local telemetry storage. Reducing default debug log volume is a correctness and operability requirement, not only presentation polish.
Debug Firehose Categories
Queries:
SELECT COUNT(*) AS logs
FROM opentelemetry_logs
WHERE body LIKE '%[jackin debug cockpit-dialog-mouse]%';Result: 21,611 logs.
SELECT COUNT(*) AS logs
FROM opentelemetry_logs
WHERE body LIKE '%session feed_pty bytes%'
OR body LIKE '%send-bytes:%'
OR body LIKE '%frame-geom:%'
OR body LIKE '%pane scroll frame:%';Result: 242,689 logs.
Interpretation: UI interaction, terminal byte, and frame-render internals should not be default OTel logs. They should become metrics, sampled debug artifacts, or deep-debug logs behind a separate explicit flag.
Error Detection Gap
Query:
SELECT severity_text, COUNT(*) AS logs
FROM opentelemetry_logs
WHERE lower(body) LIKE '%error%'
OR lower(body) LIKE '%failed%'
OR lower(body) LIKE '%panic%'
GROUP BY severity_text
ORDER BY logs DESC;Result:
| severity | logs |
|---|---|
| DEBUG | 1,594 |
| INFO | 91 |
| ERROR | 1 |
Span status query:
SELECT span_status_code, COUNT(*) AS spans, COUNT(DISTINCT trace_id) AS traces
FROM opentelemetry_traces
GROUP BY span_status_code
ORDER BY spans DESC;Result:
| status | spans | traces |
|---|---|---|
| STATUS_CODE_UNSET | 135,119 | 6,904 |
| STATUS_CODE_ERROR | 2 | 1 |
Only two error spans existed, both from BuildKit/buildx. This supports the operator's observation: jackin❯ is not sending or flagging many errors in a way Parallax can classify.
Follow-up query including EOF:
SELECT severity_text, COUNT(*) AS logs
FROM opentelemetry_logs
WHERE lower(body) LIKE '%error%'
OR lower(body) LIKE '%failed%'
OR lower(body) LIKE '%panic%'
OR lower(body) LIKE '%eof%'
GROUP BY severity_text
ORDER BY logs DESC;Result:
| severity | logs |
|---|---|
| DEBUG | 1,594 |
| INFO | 162 |
| ERROR | 1 |
Interpretation: many bodies contain failure language but are not typed outcomes. Some may be expected shutdown noise, but that is exactly why jackin❯ must emit explicit event.outcome, error.type, and expected=true|false fields instead of relying on body text.
Prefixes Are In Body
Query:
SELECT severity_text, scope_name, `parallax.run.id`, substring(body,1,120) AS body
FROM opentelemetry_logs
WHERE body LIKE '%[jackin debug%'
ORDER BY timestamp DESC
LIMIT 20;Representative rows:
| severity | scope | body |
|---|---|---|
| DEBUG | jackin_diagnostics::jsonl | [jackin debug docker] ps --filter label=... |
| DEBUG | jackin_diagnostics::jsonl | [jackin debug docker] network rm jk-...-net |
| DEBUG | jackin_diagnostics::jsonl | [jackin debug isolation] finalize: container=... preserved ... |
| DEBUG | jackin_diagnostics::jsonl | [jackin debug attach] host attach using attach-proxy ... |
Interpretation: category is present only as text. Backend cannot group without parsing body.
Span Storms
Aggregate query:
SELECT service_name, span_name, COUNT(*) AS spans, COUNT(DISTINCT trace_id) AS traces
FROM opentelemetry_traces
GROUP BY service_name, span_name
ORDER BY traces DESC, spans DESC
LIMIT 30;Representative rows:
| service | span_name | spans | traces |
|---|---|---|---|
| jackin | normal_step | 3,413 | 3,054 |
| jackin | emit_insn | 109,579 | 1,358 |
| jackin | translate | 1,462 | 1,358 |
| jackin | begin_read_tx | 1,192 | 1,135 |
| jackin | end_read_tx | 1,623 | 1,130 |
Trace inspection of usage:refresh_accounts showed hundreds of micro-spans like emit_insn, read_page_no_cache, find_frame, and translate. These are storage-engine internals, not jackin❯ workflow spans.
Desired shape:
- Keep
usage:refresh_accountsas one trace or span. - Store internal DB timings as metrics or span events:
db.statement.countdb.rows_readdb.write.countusage.accounts.refreshedusage.providerduration_ms
- Do not export storage-engine internals unless a dedicated deep-debug mode is explicitly enabled.
Sensitive Payload Risk
The live parallax logs sample contained Docker inspect JSON with environment arrays and token-shaped values. The run detail and evidence bundle also included host paths, generated Dockerfile content, image layer arrays, API-key variable names, auth paths, and provider URLs. Even if some values are local/test tokens, the telemetry path must treat them as secrets.
Required rule:
- Never export raw environment arrays, Docker inspect payloads, command output, provider auth paths, or config blobs as log bodies or attributes without redaction.
- Redact by key (
*_TOKEN,*_KEY,*_SECRET,GH_TOKEN, provider API keys, auth JSON paths where necessary) and by value pattern. - Replace large payloads with typed summaries:
container.idcontainer.namecontainer.statecontainer.exit_codemount.countenv.countimage.idnetwork.name
- Store raw artifacts only in local diagnostics files with explicit retention and access rules, not in OTel.
Code-Level Causes
Console Formatting Is Reused For Backend Logs
crates/jackin-diagnostics/src/logging.rs:
format_debug_line(category, message)returns[jackin debug {category}] {message}.emit_debug_line(category, message)passes that formatted line toactive_debug(category, &line).- The OTel log bridge exports the resulting body.
This is the enabling condition for duplicated prefixes in Parallax.
Structural fix:
- Preserve console formatting only at the terminal/file rendering boundary.
- Emit structured fields before formatting:
event.namelog.categoryjackin.componentjackin.operationjackin.stagediagnostic.tier=debug|compactmessage
- Backend log body should be concise human text without bracket prefixes.
JSONL Diagnostics Become OTel Log Records With Weak Semantics
crates/jackin-diagnostics/src/observability.rs bridges marked JSONL events (jackin_jsonl=true) into run diagnostics. emit_jsonl_event / emit_jsonl_error attach a few fields, but most semantic meaning stays in kind, stage, detail, and free-text message.
Structural fix:
- Promote stable diagnostics fields into OTel attributes.
- Reserve
bodyfor concise description. - Encode errors with OTel conventions:
- log severity
ERROR event.name=exceptionor domain error event nameexception.typeexception.messageexception.stacktracewhen availableerror.typeerror.fingerprint- active span status
ERROR
- log severity
Launch Stage Spans Reuse One Name
crates/jackin-diagnostics/src/run.rs creates spans with:
tracing::info_span!("launch_stage", stage = stage)This makes the waterfall show many identical launch_stage rows.
Structural fix:
- Use stable operation names:
launch.stage- or specific names like
launch.build_derived_image.
- If one generic span name is kept, set
otel.nameto include normalized stage:otel.name = "launch.stage"jackin.stage = "derived_image"- UI/backend can display
launch.stage derived_image.
External/Dependency Instrumentation Leaks Into Product Traces
The emit_insn/storage spans likely come from dependency tracing exposed under the broad tracing filter. They do not explain jackin❯ behavior at the operator level.
Structural fix:
- Tighten OTel export filters to jackin-owned targets for normal debug.
- Add separate opt-in
JACKIN_OTEL_INTERNAL=1for dependency/storage internals. - Prefer metrics/events for high-cardinality hot loops.
Proposed Signal Model
Runs
Run is the top-level correlation unit:
parallax.run.idservice.name=jackinservice.versionjackin.component=host|capsulejackin.workspacejackin.workspace.kindjackin.invocation.commandjackin.exit_code
Every log, span, metric, and issue event must carry parallax.run.id.
Traces And Linked Subtraces
Use one trace per operator-meaningful workflow:
launchcapsule.session.startcapsule.attachusage.refreshimage.buildauth.prewarmagent.session
Use linked subtraces when one workflow contains a large independent subsystem:
- Host launch trace links to BuildKit/buildx trace.
- Host launch trace links to capsule session trace via propagated
traceparent. - Capsule session trace links to child agent session traces.
- Usage refresh trace links to provider probes when provider work is concurrent or high-volume.
Do not force all sub-activity into one giant trace. Use span links and shared parallax.run.id / session.id / jackin.workflow.id.
Spans
Spans should be user/workflow operations, not every loop iteration:
- Good:
launch.build_derived_image,docker.container.inspect,capsule.attach,usage.refresh_accounts. - Bad as default spans:
emit_insn,read_page_no_cache,bind_and_rewrite_expr, terminal byte send/parse loops.
Rules:
- Use spans for things with duration and causal hierarchy.
- Use span events for notable details inside a span.
- Use metrics for counters/gauges/hot-loop counts.
- Use logs for human diagnostic facts.
Logs
Log body should be short and readable:
capsule attach failedAttributes should carry structure:
severity=ERROR
event.name=capsule.attach.failed
jackin.component=host
jackin.operation=capsule.attach
jackin.stage=attach
error.type=attach_error
container.name=jk-qfrehkbv-holla-thearchitect
command.name=docker
command.args_redacted=["exec","-e=JACKIN_HOST_ALT_SCREEN=1","<container>","/jackin/runtime/jackin-capsule"]
parallax.run.id=18bc74b282b0a850
trace_id=...
span_id=...Console renderer may display:
[jackin debug docker] inspect container jk-... -> runningBut backend export should be:
body="inspect container"
log.category="docker"
docker.object="container"
docker.action="inspect"
container.name="jk-..."
container.state="running"
diagnostic.tier="debug"Metrics
Move high-frequency observations into metrics:
jackin.terminal.bytes_sentjackin.terminal.cursor_movesjackin.render.duration_usjackin.render.painted_cellsjackin.usage.accounts_refreshedjackin.db.statement_countjackin.db.read_pagesjackin.docker.inspect_countjackin.errors.countbyerror.type
Metrics should be grouped by low-cardinality labels only.
Implementation Plan
Phase 1: Logs First
- Add a structured diagnostic event API beside current
clog!/cdebug!. - Keep existing console strings for terminal/file output.
- Export OTel logs with structured attributes and clean bodies.
- Add category attributes for debug logs instead of bracket prefixes.
- Redact sensitive values before any OTel export.
- Cap or artifact-route large payloads (
docker inspect, capsule log tails, raw command output). - Add tests asserting:
- console line still has old prefix where needed,
- OTel/log event body has no prefix,
log.categoryexists,- secrets are redacted,
- huge payloads are summarized.
Phase 1.5: Coverage Before Cleanup
Before removing or downsampling noisy rows, add typed coverage to the operations that currently depend on those rows for diagnosis:
- Wrap
ShellRunnerwith spans/logs for every subprocess:process.commandprocess.args_redactedprocess.cwd.kindprocess.exit_codeprocess.duration_mserror.typeon failure.
- Wrap HTTP metadata and download helpers:
- host and route class, not full secret-bearing URL where unsafe,
- status code,
- bytes,
- retry count,
- timeout/cancel/error type.
- Wrap Docker lifecycle operations:
- object type/name,
- action,
- state before/after,
- exit code,
- duration.
- Convert nested
timing_started/doneevents into real child spans or span events where the duration matters in the waterfall. - Promote launch stage details into attributes and stable span names.
- Add coverage tests using a fake runner/export sink so a regression cannot silently remove spans/log attrs for critical operations.
Phase 2: Error Semantics
- Add domain error helpers:
record_error(error_type, message, attrs)record_exception(error, attrs)
- Mark active span status
ERRORwhen a workflow fails. - Emit OTel
ERRORlogs for actual failures, notINFOdiagnostic rows. - Normalize issue fingerprints:
- include stable error type/operation/component,
- exclude container ids, run ids, temp paths, branch names, uid/gid, raw command strings.
- Replace
log_error: ...titles with domain titles.
Phase 3: Trace Shape
- Rename launch spans or set
otel.nameso waterfall rows are meaningful. - Make logs inherit the active trace/span context.
- Split huge workflows into linked subtraces:
- launch trace,
- build trace,
- attach trace,
- capsule session trace.
- Emit standard OTel span links for subtraces, and let Parallax store/render derived
parallax.parent_trace_idnavigation fields. - Drop dependency-internal spans from default export only after equivalent workflow-level spans/metrics exist.
- Convert hot-loop and storage-engine details into metrics/span events.
Phase 4: Parallax UX Contract
Even though this is mainly a jackin❯ telemetry problem, define the backend-facing contract:
- Logs page can filter by
parallax.run.id,jackin.component,log.category,jackin.operation,error.type,stage. - Issues page groups by stable fingerprint.
- Trace page displays operation + stage.
- Run page shows:
- top failures,
- linked traces,
- correlated logs,
- metrics around run,
- redacted evidence bundle.
- Trace page shows linked subtraces and backlinks using OTel span links plus Parallax-derived relation fields.
Acceptance Checks
Run a new jackin --debug under Parallax and verify:
DEBUGvolume is at least an order of magnitude lower, unless an explicit internal-firehose flag is enabled.- No OTel log body contains
[jackin debug. - No OTel log body contains raw API-token-shaped values.
parallax issue listgroups repeated capsule attach failures into one issue.- A forced attach failure creates:
- one
ERRORlog, - one error span or failed parent workflow span,
- stable
error.type, - stable issue fingerprint,
- link to run and trace.
- one
- Launch trace waterfall rows are visually distinct.
- Run detail trace has correlated logs (
Logs > 0) for stages that emitted logs. emit_insnand storage-engine spans disappear from default trace lists.
Open Questions
- Should raw local diagnostics files preserve full debug payloads when OTLP export is active, or only when
JACKIN_DIAGNOSTICS_FILE=1is set? - What is the exact allowlist for OTel-exported attributes versus local-only artifacts?
- Should Parallax issue fingerprinting be fixed in Parallax, jackin❯, or both? jackin❯ should still send stable fields so any backend can group correctly.
- What threshold should promote debug events into span events versus metrics?
- Which workflows need linked subtraces first: launch/build/attach likely first, usage refresh second.