Skip to main content

Regex and structural search over Braintrust logs, working around BTQL's lack of regex.

Project description

braintrust-grep

CI coverage python license

Tool for regex and structural search over Braintrust logs.

Braintrust's query language (BTQL) has no regex in log filters, and its LIKE/substring operator can silently match nothing on structured (JSON-object) fields.

So the reliable pattern I've found is: prefilter server-side with MATCH, pull logs scoped to a created window, then match locally in Python.

braintrust-grep does this for you while also handling the rate limit, query timeout, gzip redirects, pagination, and span deep-linking for you.

I built this while hunting down hallucinations and other discrepancies in an 'AI' (LLM) powered extraction pipeline.

Install

You can install on Github today, PyPi package coming soon.

As a CLI tool

uv tool install git+https://github.com/aflansburg/braintrust-grep

# …or run it once without installing:
uvx --from git+https://github.com/aflansburg/braintrust-grep btgrep --help

As a library:

uv add "braintrust-grep @ git+https://github.com/aflansburg/braintrust-grep"

Then set your credentials:

export BRAINTRUST_API_KEY=...     # required
export BRAINTRUST_ORG_NAME=...    # optional, for deep-links !!!! Case Sensitive it seems !!!!

Once published to PyPI this becomes uv tool install braintrust-grep / uvx braintrust-grep. The braintrust-grep command is an alias for btgrep, so uvx braintrust-grep resolves without --from.

Quickstart (CLI)

# Find spans whose output matches a regex, last 14 days (shorter time intervals are more ideal: 1h, 3h, etc.):
btgrep search -p my-project --since 14d --regex 'output=timeout|refused'

# AND several field-scoped predicates (regex + "key empty or missing"):
btgrep search -p my-project --since 7d \
    --match-fulltext output=evidence \
    --regex 'input.raw_text=user_\d+' \
    --empty output.result

Subcommands stream JSONL on stdin/stdout and diagnostics go to stderr.

It's a subcommand pattern we decided to support common in pipeline tooling like jq or the OG grep | sed | awk.

btgrep search -p my-project --since 14d --regex 'output=foo' \
  | btgrep enrich -p my-project --root-field input.doc_id --root-field input.patient_id \
  | btgrep export -o out.csv -p my-project --org MyOrg   # project id auto-resolved

You can write up a declarative spec (copy one from examples/ and fill in your project/org/patterns):

btgrep hunt --spec examples/find_errors.yaml -o out.csv

Gnarly regex? Put it in a file and reference it with @: --regex 'output=@pattern.re' (no shell-quoting pain).

Python API

from braintrust_grep import (
    BtqlClient, ClientOptions, resolve_project_id, search, enrich,
    AllOf, Regex, MetadataObjectEmpty, resolve_window,
)

client = BtqlClient(ClientOptions.from_env())
pid = resolve_project_id("my-project")
window = resolve_window("14d", "now")

predicate = AllOf(
    Regex("output", r'"symptoms"\s*:\s*\['),
    MetadataObjectEmpty("input.raw_text", keys=("pages", "documentdata"),
                        marker="=== METADATA ==="),
)
matches = list(search(
    client, pid, window=window, predicate=predicate,
    match=[("output", "evidence"), ("output", "symptoms")],  # server MATCH prefilter
))
enriched = enrich(client, matches, project_id=pid,
                  root_fields=["input.doc_id", "input.patient_id"])

Predicates

  • Regex(field, pattern, flags): regex over a dotted field (non-strings are JSON-serialized first, so you can match structure). field="*" matches the whole row.
  • EmptyOrMissing(field): true when the key is absent or the value is ""/[]/{}/null. (Regex can't express "missing".)
  • MetadataObjectEmpty(field, keys, marker=None): parses JSON found in field — a JSON column, a JSON string, or a block embedded in text after an optional marker (e.g. the field may be raw text with a preceding "=== METADATA ===") — and matches when every named key is empty-or-missing. Schema-agnostic: set field/keys/marker to your own.
  • AllOf / AnyOf / Not: compose them.

Constraints & gotchas (why this library exists)

  • 30-second query timeout. A single query over 30 seconds will return HTTP 504. Fast-path to staying under that: created-range + MATCH + cursor pagination. Wide id IN/root_span_id IN/LIKE/span-name scans will blow your rate limit budget. Enrichment uses narrow time-bucketed IN batches.
  • LIKE is blind to structured fields. output LIKE '%x%' returns 0 when output is a JSON object. Use MATCH. The query builder warns if you LIKE a structured field.
  • Rate limit ~20 requests / 60s per org (HTTP 429). The client paces itself and honors Retry-After; retries 408/429/500/502/503/504 with capped backoff.
  • Large results 303-redirect to a gzipped object: handled transparently.
  • Per-span payloads up to 20 MB (raw_text can be a whole document). The scan selects only the columns your predicates read. Never select: *.
  • Row idspan_id. Deep-links need r=<root_span_id>&s=<span_id>, not the row id. span_deeplink() takes care of this for you.

Configuration (env)

Var Purpose
BRAINTRUST_API_KEY Auth (required)
BRAINTRUST_ORG_NAME Org slug for deep-links
BRAINTRUST_BTQL_URL Override the BTQL endpoint

ClientOptions exposes min_request_interval, max_retries, page_size, retry_statuses, backoff knobs, etc. .env is loaded automatically if python-dotenv is installed (never required).

Development

uv sync --extra dev                 # install with dev + test deps
uv run ruff check . && uv run ruff format --check .

Running the tests

The suite is offline. No Braintrust credentials or network required. HTTP is faked with httpx.MockTransport and time with a FakeClock, so it runs in well under a second.

uv run pytest                       # all tests
uv run pytest --cov                 # with a coverage report
uv run pytest -v                    # verbose (list each test)
uv run pytest tests/test_client.py  # one file
uv run pytest -k "retry or gzip"    # tests matching an expression
uv run pytest -x -q                 # stop at first failure, quiet

TBD

Async client; parallel bucket fetching (the rate limit makes concurrency low-value); result caching/resume; non-project_logs sources (experiments, datasets); a custom-predicate plugin system beyond --spec; an interactive TUI.

Contributing

Issues and PRs welcome. Please:

  1. Fork and branch off main.
  2. uv sync --extra dev, then keep ruff check, ruff format, and pytest green.
  3. Add a test for any behavior change (the suite is offline — mock HTTP with httpx.MockTransport and time with FakeClock; see tests/conftest.py).
  4. Open a PR describing the change and, for a bug fix, the failing case it covers.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

braintrust_grep-0.1.0.tar.gz (30.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

braintrust_grep-0.1.0-py3-none-any.whl (27.4 kB view details)

Uploaded Python 3

File details

Details for the file braintrust_grep-0.1.0.tar.gz.

File metadata

  • Download URL: braintrust_grep-0.1.0.tar.gz
  • Upload date:
  • Size: 30.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for braintrust_grep-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8d54c290b3c6ab1c67b0a3f22e9d4b4caec5afe6006b8532d10ca4650e4d2543
MD5 3dcddadcc40c928936092cc0b7696247
BLAKE2b-256 a205466bedf570d0c8cdd9b2fe1ae5e026c22e8f8ee6fbda9601c54c21cac544

See more details on using hashes here.

Provenance

The following attestation bundles were made for braintrust_grep-0.1.0.tar.gz:

Publisher: publish.yml on aflansburg/braintrust-grep

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file braintrust_grep-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: braintrust_grep-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 27.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for braintrust_grep-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d6add100403331e7e74f68c0d1cb8ac7c2bfde314ece6e304e437b922bd10697
MD5 428f5e380f5ff3783cda45126a33968b
BLAKE2b-256 403832ee4ec39c667163b0c311d73a2b8b2de226e98d2c890e91fdd94f91bec6

See more details on using hashes here.

Provenance

The following attestation bundles were made for braintrust_grep-0.1.0-py3-none-any.whl:

Publisher: publish.yml on aflansburg/braintrust-grep

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page