Skip to content

feat(logger): add new logging apis - #759

Merged
Abhijeet Prasad (AbhiPrasad) merged 19 commits into
mainfrom
abhi-logs
Sep 22, 2026
Merged

Abhijeet Prasad (AbhiPrasad) merged 19 commits into
mainfrom
abhi-logs

Conversation

@AbhiPrasad

@AbhiPrasad Abhijeet Prasad (AbhiPrasad) commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

resolves https://linear.app/braintrustdata/issue/SDK-341/add-logging-api-to-python-sdk
ref https://app.notion.com/p/braintrustdata/Braintrust-Logs-Schema-and-SDK-API-3def7858028980e29a83cddb3f81203b
schema https://github.com/braintrustdata/braintrust/pull/20692

AI Summary

Add first-class log emission to the project logger so applications can create independent type="log" rows without constructing spans manually.

  • Add Logger.emit_log(body, level, metadata).
  • Add trace(), debug(), info(), warn(), error(), and fatal() convenience methods.
  • Store the canonical severity in span_attributes["log_level"].
  • Add SpanTypeAttribute.LOG.
  • Support named Python format parameters while retaining the original template and parameter values in metadata.
  • On Python 3.14+, support t-string objects with embedded interpolation values, conversions, and format specifications.

Usage

Emit directly or use a severity helper:

logger.error(
    "Payment failed",
    metadata={"payment_id": "pay_123"},
)

logger.emit_log(
    body="Retrying payment",
    level="info",
    metadata={"attempt": 2},
)

Log methods also accept named str.format parameters:

logger.info(
    "User {user_id} paid {amount:.2f}",
    user_id="user_123",
    amount=12.5,
    metadata={"source": "checkout"},
)

On Python 3.14+, log methods also accept t-strings. Their interpolation values are already embedded, so no separate keyword parameters are needed:

user_id = "user_123"
amount = 12.5

logger.info(
    t"User {user_id} paid {amount:.2f}",
    metadata={"source": "checkout"},
)

Both forms store the rendered message in output:

User user_123 paid 12.50

They also retain the stable template and its raw parameters for querying and grouping. Canonical severity follows the updated logs schema and lives in span_attributes, separate from user metadata:

{
    "output": "User user_123 paid 12.50",
    "span_attributes": {
        "type": "log",
        "log_level": "info",
    },
    "metadata": {
        "source": "checkout",
        "braintrust.template": "User {user_id} paid {amount:.2f}",
        "braintrust.template.parameter.user_id": "user_123",
        "braintrust.template.parameter.amount": 12.5,
    },
}

Missing str.format parameters remain as placeholders, and malformed templates fall back to the original body so formatting errors do not disrupt application code. T-string conversions and format specifications follow f-string rendering semantics; unsupported formatting leaves the affected placeholder intact. Bodies may remain non-string JSON values when no template parameters are supplied.

T-string support is loaded only on Python 3.14+, preserving SDK compatibility with Python 3.10 through 3.13.

Trace correlation

Each log has a unique row ID and span ID unless it is correlated with an active span. A logger seeds one baseline trace ID when it is created, so consecutive unscoped logs from that logger remain grouped without grouping logs from separate logger instances.

helper -> emit_log -> type="log" row
                     |-- active span
                     |     trace_id = active trace_id
                     |     span_id  = active span_id
                     |
                     `-- no active span
                           trace_id = logger baseline trace_id
                           span_id  = newly generated span_id

This works with both native Braintrust spans and active OpenTelemetry spans through the existing context manager abstraction.

Python logging handler

The opt-in BraintrustLogHandler forwards standard-library logging records without enabling automatic instrumentation:

import logging

from braintrust import BraintrustLogHandler, init_logger

handler = BraintrustLogHandler(init_logger(project="my-project"))
logging.getLogger().addHandler(handler)

The handler preserves formatted messages, original timestamps, exceptions, template parameters, extra fields, logger/source metadata, active span correlation, and normalized severity in span_attributes.log_level. Logs from Braintrust and its urllib3 transport are excluded to prevent recursive forwarding.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-22T18:22:28.542757Z ab5d9a1 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd6b7345e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

)
rendered_metadata["braintrust.template"] = body
try:
rendered_body = body.format_map(_LogTemplateParameters(parameters))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve formatting for missing fields with format specs

When an omitted placeholder has a conversion or format specifier, such as logger.info("{user} owes {amount:.2f}", user="alice"), __missing__ supplies the string "{amount}", formatting that string as a float raises, and this broad fallback restores the entire original template. Consequently even supplied parameters are left uninterpolated, contrary to the documented behavior that only missing parameters remain as placeholders. Preserve the missing field's conversion/specifier instead of abandoning all rendering.

Useful? React with 👍 / 👎.

if parameters:
if not isinstance(body, str):
raise TypeError("Log body must be a string when template parameters are provided")
rendered_metadata = dict(metadata) if metadata is not None else {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize supported metadata before adding template attributes

When a caller combines template parameters with Pydantic-style metadata accepted by the rest of the logger API, this direct conversion can raise TypeError: an object implementing the supported model_dump() or dict() protocol is not necessarily iterable. The same metadata works when no template parameters are supplied because the normal event sanitizer handles those protocols, so logger.info("User {id}", metadata=model, id=...) unexpectedly emits no log. Retain the Metadata input contract and normalize it before merging the template attributes.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 657bcd32df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".


def info(self, body: Any, metadata: dict[str, Any] | None = None, **parameters: object) -> str:
"""Capture a log at OpenTelemetry INFO severity."""
return self.emit_log(body=body, level="info", metadata=metadata, **parameters)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve level as a template parameter in helpers

When a severity helper is given a template parameter named level, such as logger.info("Connected at {level}", level="database"), the helper collects it in parameters and then passes it alongside the fixed level="info" argument, causing Python to raise TypeError: got multiple values for keyword argument 'level' before any log is emitted. Since named placeholders are otherwise advertised without restrictions, pass template parameters through a non-colliding container or render them before forwarding.

Useful? React with 👍 / 👎.

Comment thread py/src/braintrust/logs.py Outdated
Comment on lines +43 to +44
parameters = record.args.items() if isinstance(record.args, dict) else enumerate(record.args)
metadata.update({f"braintrust.template.parameter.{key}": value for key, value in parameters})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve mapping keys in logging template metadata

When standard logging interpolation uses a non-dict mapping such as collections.UserDict (logger.info("%(user)s", UserDict(user="alice"))), LogRecord stores that mapping directly in record.args and formats the message successfully. This branch instead treats it as positional arguments and enumerates its keys, recording braintrust.template.parameter.0 = "user" rather than braintrust.template.parameter.user = "alice", so the emitted template attributes are silently incorrect; recognize general Mapping instances here.

Useful? React with 👍 / 👎.

@AbhiPrasad Abhijeet Prasad (AbhiPrasad) changed the title feat(logger): add OpenTelemetry-compatible log emission feat(logger): add new logging apis Sep 17, 2026
Add `Logger.emit_log()` so applications can emit independent `type="log"`
rows without constructing spans manually. Correlate rows with the active
Braintrust or OpenTelemetry context when available.

Seed each logger with a baseline trace ID for unscoped logs. This keeps
consecutive logs from one logger together while preserving unique row and span
IDs, and avoids grouping logs emitted by separate logger instances.

Map the six base OpenTelemetry severities into `context.otel.log` and add
`trace()`, `debug()`, `info()`, `warn()`, `error()`, and `fatal()` helpers.

    logger.error("Payment failed", metadata={"payment_id": "pay_123"})

    helper -> emit_log -> `type="log"` row
                         |-- active span: reuse trace/span IDs
                         `-- no span: reuse logger trace, generate span ID
Allow `emit_log()` and severity helpers to interpolate named parameters with
Python format strings. Preserve the original template and parameter values in
`braintrust.template` metadata so repeated messages remain queryable by their
stable structure.

Missing placeholders and malformed templates remain unchanged so logging does
not disrupt application code.
Expose an opt-in standard-library logging handler that forwards formatted records, source metadata, timestamps, and template parameters through the Braintrust logging API while avoiding recursive SDK transport logs.
Render string.templatelib.Template bodies with their embedded values while
retaining the reconstructed template and raw parameters in metadata.

Load templatelib only on Python 3.14+ so the SDK remains compatible with
Python 3.10 through 3.13.
Comment thread py/src/braintrust/logs.py
@realark

Copy link
Copy Markdown
Contributor

🚀

Align emitted rows with the updated logs schema by moving canonical severity from
metadata to span_attributes.log_level. This keeps user and template metadata free
of schema-owned fields.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75a1b2aecb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logger.py Outdated
Comment on lines +172 to +173
parameter_name = interpolation.expression.strip() or str(index)
parameters[parameter_name] = interpolation.value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve repeated t-string interpolation values

When a t-string repeats an expression that produces different values, such as it = iter([1, 2]); logger.info(t"{next(it)} {next(it)}"), both interpolations use the same parameter_name, so the second assignment silently overwrites the first. The rendered body remains "1 2", but the retained metadata contains only braintrust.template.parameter.next(it) = 2, losing a raw parameter the API promises to retain; use occurrence-specific keys when an expression is repeated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6e6c8b8. Repeated expressions now receive occurrence-specific keys in evaluation order (for example, next(it).0 and next(it).1), so every raw value is retained. Unique expressions keep their existing unsuffixed keys. Added a regression test for the 1 2 case.

logging.shutdown holds a handler lock while flush waits for HTTP workers. Filter
Braintrust and urllib3 records through Handler.filter so those workers never try
to acquire the same lock.
Suffix repeated interpolation expressions by occurrence so each evaluated value is
preserved in log metadata. Unique expressions keep their existing parameter keys.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e6c8b84c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

name="Log",
type=SpanTypeAttribute.LOG,
span_attributes={"log_level": level},
start_time=captured_at,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve zero-valued record timestamps

When the handler forwards a replayed or synthetic LogRecord whose created value is exactly 0, this passes 0 into SpanImpl, where start_time or time.time() treats it as absent. The resulting row retains created and metrics.end at the Unix epoch but records metrics.start as the current time, corrupting the promised original timestamp and producing an invalid duration; handle zero explicitly rather than relying on the span constructor's falsy fallback.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1289051. SpanImpl now treats only None as an absent start time, so a valid 0 timestamp is preserved. Added a regression test that sends a LogRecord with created = 0 through BraintrustLogHandler and verifies created, metrics.start, and metrics.end all remain at the Unix epoch.

Treat only None as an absent start time so replayed epoch log records keep created, start, and end timestamps aligned.
Keep the internal log span name while excluding it from serialized log rows so the schema contains only log-specific span attributes.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73bf23d97d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logs.py Outdated
Comment on lines +48 to +49
parameters = record.args.items() if isinstance(record.args, dict) else enumerate(record.args)
metadata.update({f"braintrust.template.parameter.{key}": value for key, value in parameters})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve positional mappings as a single template argument

When standard logging receives a mapping as its sole positional value, such as logger.info("payload=%s", {"id": 1}), LogRecord unwraps the one-element tuple and stores the dictionary directly in record.args, even though %s still formats the whole dictionary as argument 0. This branch instead interprets every dictionary as named interpolation and records braintrust.template.parameter.id = 1, losing the actual raw parameter braintrust.template.parameter.0 = {"id": 1}. Distinguish named %(key)s interpolation from a mapping supplied to a positional placeholder before constructing the metadata.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6841bdf. Mapping-shaped LogRecord.args are now expanded by key only when the template contains an active named %(key) placeholder. A mapping used with a positional %s is retained as braintrust.template.parameter.0, and escaped %%(key)s is handled as positional. Added regression coverage for all three cases.

Comment thread py/src/braintrust/logs.py Outdated
"asctime",
"message",
}
_IGNORED_LOGGER_PREFIXES = ("braintrust", "urllib3")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not suppress unrelated urllib3 application logs

When this handler is attached to a root or application logger, the prefix filter drops every urllib3 record before emission, including requests made by the application to its own services rather than by Braintrust. Applications commonly rely on urllib3.connectionpool logs for HTTP diagnostics, so enabling the Braintrust handler silently excludes that entire class of user logs. Scope recursion prevention to Braintrust transport activity rather than filtering the global urllib3 namespace.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d1073b. We no longer suppress the global urllib3 namespace. Braintrust HTTPConnection and Transport request scopes now set a ContextVar guard, and BraintrustLogHandler filters records only while that SDK transport guard is active (plus the existing braintrust logger namespace). Added regressions showing application urllib3 records are forwarded and SDK transport records are rejected before the handler lock, preserving the shutdown deadlock fix.

Mark Braintrust HTTP request scopes explicitly so the log handler suppresses only SDK transport activity instead of the entire urllib3 namespace.
Inspect percent-style placeholders before expanding mapping arguments so dictionaries passed to positional placeholders remain a single raw parameter.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6841bdfe31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logs.py Outdated
Comment on lines +62 to +66
if isinstance(record.args, Mapping) and _uses_named_percent_parameters(record.msg):
parameters = record.args.items()
else:
positional_args = (record.args,) if isinstance(record.args, Mapping) else record.args
parameters = enumerate(positional_args)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain mappings used by mixed percent placeholders

The new named-placeholder discriminator still loses positional parameter metadata for valid mixed templates. For example, logger.info("%s %(id)s", {"id": 1}) is rendered successfully by standard logging because the mapping is consumed both as the %s value and by %(id)s, but this branch records only braintrust.template.parameter.id and omits braintrust.template.parameter.0 for the positional placeholder. Handle mixed positional/named templates by retaining the mapping in both applicable forms.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 2b16d11. Percent placeholders are now scanned for positional and named usage independently. For %s %(id)s with {"id": 1}, metadata retains both braintrust.template.parameter.0 = {"id": 1} and braintrust.template.parameter.id = 1. Existing positional-only, named-only, and escaped-percent cases remain covered.

Detect positional and named percent placeholders independently so mixed mapping templates preserve both the raw positional value and keyed parameters.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b16d113dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logs.py Outdated

class _InternalLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
return not _is_ignored_logger(record.name) and not _is_internal_http_transport()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate the transport marker through queued records

When this handler is used behind a standard QueueHandler/QueueListener, filter() executes on the listener thread, so this ContextVar reads its default False even for an urllib3 record created inside a guarded Braintrust request. Fresh evidence beyond the prior fix is that the marker exists only in the originating execution context and is not attached to the LogRecord. With urllib3 debug or retry logging enabled, the handler can therefore re-ingest Braintrust transport logs and each upload can generate another record, causing an unbounded logging cycle; carry an internal marker on the record or otherwise make recursion filtering survive asynchronous dispatch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 751ef63. BraintrustLogHandler now installs a chain-preserving LogRecordFactory that stamps records created while the Braintrust HTTP transport guard is active. The handler filter checks that persisted marker as well as the live ContextVar, so QueueHandler/QueueListener thread handoff retains recursion protection. Added an end-to-end queued-record regression that verifies the marker survives QueueHandler.prepare and the listener suppresses the record.

Stamp guarded transport records at creation time so QueueHandler and QueueListener dispatch cannot lose recursion protection across thread boundaries.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 751ef63974

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logger.py Outdated
valid_levels = ", ".join(_LOG_LEVELS)
raise ValueError(f"Invalid log level {level!r}. Expected one of: {valid_levels}")

span_info = self.state.context_manager.get_current_span_info()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Carry span context through queued log records

When BraintrustLogHandler runs behind a standard QueueHandler/QueueListener, this lookup executes on the listener thread, whose context does not contain the Braintrust or OpenTelemetry span that was active when the record was created. Logs emitted inside a span are therefore assigned the logger's baseline trace and a new span ID instead of being correlated with the active span; snapshot the originating span/trace IDs onto the LogRecord before it is queued and use them here.

Useful? React with 👍 / 👎.

Comment thread py/src/braintrust/logs.py Outdated
if key not in _STANDARD_LOG_RECORD_ATTRIBUTES and not key.startswith("_")
}

if record.args and isinstance(record.msg, str):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Snapshot template arguments before queue preparation

When this handler is consumed by a standard QueueListener, QueueHandler.prepare() renders the message and then sets record.args to None before enqueueing it. Consequently this condition is false for every queued parameterized log, so the rendered body survives but braintrust.template and all raw braintrust.template.parameter.* metadata are silently lost; preserve the template and arguments on the record at creation/enqueue time.

Useful? React with 👍 / 👎.

Snapshot template arguments and active span IDs when a LogRecord is created so QueueHandler preparation and QueueListener thread boundaries do not discard them.

Key span snapshots by Braintrust state so queued records remain correlated with the handler's originating context.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8de8a57db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logs.py Outdated
Comment on lines +44 to +45
with _LOG_RECORD_FACTORY_LOCK:
context_managers = tuple(_CONTEXT_MANAGERS.items())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset the record-factory lock after fork

When a multithreaded application forks while another thread holds _LOG_RECORD_FACTORY_LOCK, the child inherits the lock in its acquired state but not the thread that can release it. Because the installed process-global record factory acquires this lock for every subsequent record, the child then hangs permanently on its first log call. Register an os.register_at_fork child reset for this lock or avoid acquiring a non-fork-aware lock in the record factory.

Useful? React with 👍 / 👎.

Tag urllib3 records at their source only while Braintrust HTTP transport is active. This keeps the marker queue-safe without suppressing unrelated application logs that happen to run in the same transport context.

Keep tagged records available to non-Braintrust handlers while rejecting them before the Braintrust handler lock.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ff09af1a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logs.py

def record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
record = current_factory(*args, **kwargs)
if record.args and isinstance(record.msg, str):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep queued log records pickleable

When a multiprocessing-backed QueueHandler processes a parameterized record containing a non-pickleable argument, such as a lambda, lock, or open file, this attribute retains the original object after QueueHandler.prepare() renders the message and clears record.args. The queue feeder then cannot pickle the prepared record, so the log is dropped even though standard queued logging supports such arguments after rendering; preserve the metadata without attaching arbitrary raw objects to the queued LogRecord.

Useful? React with 👍 / 👎.

Comment thread py/src/braintrust/logs.py
_IGNORED_LOGGER_PREFIXES = ("braintrust",)
_URLLIB3_TRANSPORT_LOGGERS = ("urllib3.connectionpool", "urllib3.connection")
_INTERNAL_HTTP_TRANSPORT_RECORD_ATTRIBUTE = "_braintrust_internal_http_transport"
_LOG_RECORD_FACTORY_ATTRIBUTE = "_braintrust_log_record_factory"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark urllib3 retry records as internal transport logs

When a Braintrust upload is retried with urllib3 debug logging enabled, urllib3 emits its retry message from urllib3.util.retry, which is not one of these two source loggers and therefore never receives the transport marker. A root BraintrustLogHandler consequently re-ingests that internal record; repeated upload failures can continually generate additional rows, and a shutdown flush waiting on the HTTP worker can deadlock when that worker tries to acquire the handler lock. Apply the guarded marker to all urllib3 transport records rather than only these two module loggers.

Useful? React with 👍 / 👎.

Replace the inherited logging-hooks lock in forked children so the process-global LogRecord factory cannot deadlock when another parent thread held the original lock.

Cover the failure with a bounded POSIX fork regression that verifies the child can create a record.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab5d9a1226

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logs.py
Comment on lines +38 to +41
def _safe_template_parameter(value: Any) -> Any:
if isinstance(value, (str, float, int, bool)):
return value
return _safe_parameter_repr(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve JSON-safe template values in snapshots

When a normal logging call passes a structured or null argument, such as logger.info("items=%s", [1, 2]) or logger.info("value=%s", None), the installed record factory always snapshots it through this function, so the retained template parameter becomes the string "[1, 2]" or "None" even without a queue. This loses the raw JSON type and prevents users from filtering or inspecting structured parameters; preserve values that are already safely serializable/pickleable and fall back to repr only for unsafe objects.

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit 5cce65d into main Sep 22, 2026
83 checks passed
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