feat: env-driven LogSettings + OTEL-ready logging - #503
Open
AlexanderZ-Band wants to merge 14 commits into
Conversation
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>
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
marked this pull request as ready for review
July 31, 2026 04:23
…ai-band-integration-ban-INT-1116' into HEAD # Conflicts: # examples/a2a_gateway/demo_orchestrator/__main__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LogSettings(BAND_LOG_*) as the opt-in entry point for process-wide Band logging, with file rotation, style/stream knobs, and JSON overridesconfigure_logging/build_logging_configwith typed styles and richer sinks; migrate examples, CLIs, Docker runners, and desktop/ACP paths onto the shared APILoggingHandler, emits JSON trace-correlation fields, passes through pydantic-aiinstrument=, addsexamples/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 -vBAND_LOG_LEVEL=DEBUGand optionalBAND_LOG_FILEexamples/opentelemetry/, keep-handlers + pydanticinstrument=)main