Skip to content

feat: env-driven LogSettings + OTEL-ready logging - #503

Open
AlexanderZ-Band wants to merge 14 commits into
feat/intel-claude-desktop-claudeai-band-integration-ban-INT-1116from
feat/environment-driven-logsettings-for-sdk-wide-loggin-INT-1153
Open

feat: env-driven LogSettings + OTEL-ready logging#503
AlexanderZ-Band wants to merge 14 commits into
feat/intel-claude-desktop-claudeai-band-integration-ban-INT-1116from
feat/environment-driven-logsettings-for-sdk-wide-loggin-INT-1153

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add validated LogSettings (BAND_LOG_*) as the opt-in entry point for process-wide Band logging, with file rotation, style/stream knobs, and JSON overrides
  • Expand configure_logging / build_logging_config with typed styles and richer sinks; migrate examples, CLIs, Docker runners, and desktop/ACP paths onto the shared API
  • Document the env surface in the README and cover settings + logging behavior with unit tests
  • OpenTelemetry (in scope): host owns providers/exporters; Band keeps root handlers for OTEL LoggingHandler, emits JSON trace-correlation fields, passes through pydantic-ai instrument=, adds examples/opentelemetry/ demo + README matrix (no Band TracerProvider / no sbx collector)

Closes INT-1153

Test plan

  • uv run pytest tests/test_log_settings.py tests/test_logging_config.py tests/test_band_import.py tests/integrations/desktop_app/test_logs.py tests/docker/launcher/test_exec.py -v
  • Spot-check an example with BAND_LOG_LEVEL=DEBUG and optional BAND_LOG_FILE
  • OTEL demo / coexistence tests once landed (examples/opentelemetry/, keep-handlers + pydantic instrument=)
  • Confirm stack: this PR targets the INT-1116 desktop branch, not main

Centralize BAND_LOG_* configuration behind validated settings so apps,
examples, and CLIs share one opt-in logging entry point with file
rotation and per-logger overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>
@linear-code

linear-code Bot commented Jul 30, 2026

Copy link
Copy Markdown

INT-1153

AlexanderZ-Band and others added 12 commits July 30, 2026 20:14
Drop None fields so optional CLI levels leave BAND_LOG_* intact, and
collapse the repeated kwargs dance across example setup helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
Explain the embed vs application entrypoint choice in the README and
API docstrings so callers know when their own INFO logs stay silent.

Co-authored-by: Cursor <cursoragent@cursor.com>
JSON records now always include otelTraceID, otelSpanID, otelTraceSampled,
and otelServiceName — the attributes LoggingInstrumentor injects. They
serialize as null in an uninstrumented process, so a log pipeline reads one
shape whether or not the host turned tracing on.

Band still creates no provider, processor, or exporter, and takes on no
OpenTelemetry dependency. A host telemetry handler must be attached after
LogSettings.configure(), since dictConfig is non-incremental and replaces
root's handlers; the test pins both orders so the rule cannot rot.

Also raises the python-json-logger floor to 3.1.0, the release that moved
pythonjsonlogger.jsonlogger to the pythonjsonlogger.json module the formatter
is configured against, and adds it to the dev extra so the JSON tests run
instead of skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
PydanticAIAdapter takes an `instrument` argument and hands it to the agent:
None inherits Agent.instrument_all(), False opts this agent out of it, True
uses the host's global TracerProvider, and InstrumentationSettings customizes.

pydantic-ai 2.18's Agent() has no instrument parameter, so it is assigned to
the public property after construction — which also keeps the ProcessHistory
capability that strips content:null responses. A test runs a real agent
against an in-memory exporter to prove both survive together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
examples/opentelemetry runs the host half end to end — one shared Resource,
both providers, console exporters (no collector), trace-context injection,
and a clean flush — then a normal Pydantic AI agent on top of it. A log line
emitted inside a span reports that span's ids, and the framework's own spans
arrive alongside.

The README section says who owns what, spells out the setup order and why
dictConfig forces it, and classifies each adapter by where its model call
actually happens: bridged natively (Pydantic AI), instrumented by the
framework itself (Parlant, Google ADK), instrumented by the caller who built
the object (LangGraph, Agno, CrewAI Flow), instrumented at the provider SDK
(Anthropic, Gemini, CrewAI), or out of process entirely — where Band's
process has no model spans to emit. Containers and sandboxes stay a
documented gap rather than a half-answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
Found by review of this branch; each is reproducible and none was covered.

- band-acp pinned to stderr. stdout carries its JSON-RPC frames, so
  BAND_LOG_STREAM=stdout — now a documented, process-wide knob — put log lines
  into the protocol stream and killed the editor's session. The desktop stdio
  server and band-trigger already pinned it.
- A size cap with no backups is rejected, and BAND_LOG_BACKUPS defaults to 1.
  RotatingFileHandler with backupCount=0 reopens the same file in append mode,
  so "rotate at 1 MB" grew without bound — the opposite of the request.
- A per-logger level from extra_loggers now reaches the file sink. When console
  and file share a level only the file handler was pinned, so an explicit
  {"httpx": "DEBUG"} printed to the console and was dropped from the log file.
- Subclass defaults keep the base validation. Redeclaring a field to move its
  default replaced the annotation and silently dropped the case-insensitive
  coercion, so BAND_LOG_STREAM=STDOUT raised for a consumer subclass while
  working for LogSettings; the coercion moved to an inherited validator, and
  DesktopLogSettings regained its bounds.

Also restores the signature-parity guard between configure_logging and
build_logging_config, which forward fifteen kwargs by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
Four provisioning scripts pass fmt="%(message)s" without pinning a style, so
exporting BAND_LOG_CONSOLE_STYLE=json killed them at import with "fmt is only
supported with style='standard'". They now pin the style alongside the format,
as the Docker launcher already did.

The claude_sdk_docker runner had its logging call inserted above main()'s
docstring, leaving a dead string expression and no __doc__.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
Review of the OTEL commits, verified against the code:

- The helper no longer publishes global providers. set_tracer_provider takes
  only the first call of a process, so a helper that published its provider and
  then shut it down on exit left every later instrument=True in that process
  writing into a dead one — silently. Each consumer is handed the provider
  instead: the instrumentor, the log handler, and the adapter's
  InstrumentationSettings. A host that does own the globals still gets the
  shorter instrument=True, which the README says.
- The demo demotes the chatty transport loggers. for_application() raises the
  root level, which otherwise lets httpx, httpcore, and the channels client
  narrate every request and frame at INFO — into JSON logs and the OTEL log
  exporter both.
- The correlation-schema claim is now accurate. The four keys are in the
  *default* JSON field set; passing json_fields replaces it wholesale, which
  the docstring, README, and example README each said otherwise. A test pins
  the replacement contract.
- The correlation test parses the JSON lines out of stderr rather than the
  whole stream, so a warning sharing the stream cannot break it.
- instrument=True is covered, by resolving pydantic-ai's ambient provider
  lookup at the seam it reads.

Rejected: the same "missing chatty_logger_levels()" claim against the Docker
launcher. That process resolves config, shells out to git and uv, then
execve()s the customer entrypoint; it never imports httpx, httpcore, or the
channels client, so there are no transport loggers in it to demote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
Ran the demo against the live platform with a real model turn. A turn is one
trace: invoke_agent over a chat span per model call, with gen_ai attributes and
token counts, plus an execute_tool span for the band_send_message reply — more
than the README claimed, and worth naming so the expected output matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
…tting

Three findings from reviewing the desktop commits, each reproduced first.

- A roleless relay no longer fails the launch. If neither leadership nor a
  follower socket was reachable within the start timeout, start() raised and
  the MCP server exited — Desktop showing a dead server. Yet every read path
  works over REST, the footer, RelayStatus.warning and the docs all describe a
  "WebSocket down · polling" mode, and the identical failure arriving a minute
  later degrades gracefully. Only the first thirty seconds were fatal. Now it
  logs the reason and serves polled reads while the supervisor keeps retrying.

- Truncation no longer decides who a message was for. read() cut the content to
  BAND_MAX_MESSAGE_CHARS and *then* asked whether it addressed the agent, so a
  mention past the limit left the message unaddressed and out of
  pending_requests. Band's own messages carry the mention in metadata, which is
  checked first — this only lost the case the content fallback exists for.

- The attention default is stated once. The session opened rooms user_first
  while the transcript model and the view both defaulted to room_first, so the
  widget announced a monitoring contract the server was not running until the
  first payload corrected it. DEFAULT_ATTENTION now owns the value, and a test
  pins the view's own copy to it the way maxWatchS is already pinned.

Two more findings are recorded but deliberately not acted on: the extra page-1
read per tick in multi-page rooms (no fix that is cleaner than the waste), and
the per-mount session maps that never evict (bounded in practice, and the
seen-instance set is what stops a retired widget stealing the room back).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
…really is

examples/slack/dev_bridge.py exists to debug the Slack bridge, and the move to
LogSettings cost it exactly the logs it was for: the basicConfig it replaced put
root at INFO, so slack_sdk narrated every connection attempt, while a bare
LogSettings() elevates only band and leaves root at WARNING. It now uses the
shared examples/slack/setup_logging, which already raises slack_sdk with band —
the same call 01_basic_bot.py makes.

The Docker launcher's configure_logging() promised stderr without pinning it, so
BAND_LOG_STREAM could move it. Corrected the docstring rather than the code:
nothing reads this process's stdout — it logs and then execs the customer
entrypoint — so unlike band-acp and band-trigger, whose stdout carries JSON-RPC
frames and command output, there is no protocol to protect, and an operator
sending container logs to stdout is asking for something reasonable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
The ACP stderr-pin test read handler.stream off every root StreamHandler.
FileHandler subclasses StreamHandler, so an ambient BAND_LOG_FILE put a file
stream in that list and failed the assertion even though console logging was
correctly pinned; an ambient rich console style emptied the list instead. Assert
the property that matters: a probe line reaches stderr and not the JSON-RPC
transport, with BAND_LOG_* scoped by band_log_env.

band trigger pins STDERR for the same reason — stdout carries only the room ID —
and had no such guard, since the existing stdout test emits no log record. Add
one that logs during the run.

Rotation coverage collapses into the behavior test: the dictConfig shape assert
restated the builder, and the rollover test ran at the default backup count, so
neither proved a requested backup_count reaches the handler. Rotating with
backup_count=2 and asserting exactly two backups survive covers both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqZ7dPugC4rTKzmyxihGKg
@AlexanderZ-Band
AlexanderZ-Band marked this pull request as ready for review July 31, 2026 04:23
@AlexanderZ-Band AlexanderZ-Band changed the title feat: environment-driven LogSettings for SDK logging feat: env-driven LogSettings + OTEL-ready logging Jul 31, 2026
@AlexanderZ-Band
AlexanderZ-Band requested a review from a team July 31, 2026 09:23
…ai-band-integration-ban-INT-1116' into HEAD

# Conflicts:
#	examples/a2a_gateway/demo_orchestrator/__main__.py
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.

2 participants