NeatlogsNeatlogs
Custom Instrumentation

Log Capture

Capture structured log steps, stdlib logging calls, and print() output as LOG spans inside traces.

Neatlogs captures logs as LOG spans — children of the active span — so they appear inline in the trace timeline in the dashboard. There are three mechanisms, and you can use any combination.

Log capture is opt-in. Enable it by passing capture_logs=True to neatlogs.init():

neatlogs.init(
    api_key="...",
    capture_logs=True,  # required to enable all log capture
)

Without capture_logs=True, none of the three mechanisms below will capture LOG spans.

1. neatlogs.log() — Structured Template Messages

neatlogs.log() is the primary way to emit a named, structured step inside a span. It stores the message template as the span name (low-cardinality, searchable) and each keyword argument as a log.{key} attribute.

import neatlogs

@neatlogs.span(kind="CHAIN")
def rag_pipeline(query: str) -> str:
    docs = retrieve(query)
    neatlogs.log("retrieved {count} docs, top score {score:.2f}",
                 count=len(docs), score=docs[0]["score"])

    reranked = rerank(query, docs)
    neatlogs.log("reranked to {count} docs", count=len(reranked))

    return generate(query, reranked)

Signature

neatlogs.log(msg_template, /, level="info", **data)
ParameterDescription
msg_templateMessage template with {key} placeholders. Stored as the span name.
levelLog level: "info", "debug", "warning", "error". Default: "info".
**dataKey-value pairs rendered into the template and stored as log.{key} attributes.

Templates use Python's str.format_map() syntax — single braces: {count}, not {{count}}.

Debug echo

When neatlogs.init(debug=True), every neatlogs.log() call is echoed to stderr immediately so you can see steps in the terminal without opening the dashboard:

[neatlogs] 12:34:56  LOG  retrieved 42 docs, top score 0.94  count=42  score=0.94

Important

neatlogs.log() must be called inside an active span (@neatlogs.span or with neatlogs.trace()). Calls outside a traced block are dropped — there is no active trace to attach them to.


2. stdlib logging.*() — Auto-Captured

When capture_logs=True, any logging.debug(), logging.info(), logging.warning(), logging.error() call inside a @span or with neatlogs.trace(): block is captured automatically — no code changes needed. All levels are captured by default.

import logging
import neatlogs

logger = logging.getLogger(__name__)

@neatlogs.span(kind="AGENT")
def classify_intent(message: str) -> str:
    intent = run_classifier(message)

    logger.debug("Classifier input length: %d", len(message))     # captured
    logger.info("Intent classified: %s", intent)                  # captured
    if intent == "inappropriate":
        logger.warning("Inappropriate content — routing to rejection")  # captured

    return intent

To restrict capture to higher-severity levels only, raise the threshold at init time:

neatlogs.init(
    ...,
    log_level="WARNING",  # only WARNING, ERROR, CRITICAL
)

3. capture_stdout=True — Print Capture

With capture_logs=True enabled, pass capture_stdout=True to @neatlogs.span or with neatlogs.trace() to capture every print() line inside that block as a LOG span. Existing code doesn't need to change.

@neatlogs.span(kind="AGENT", capture_stdout=True)
def run_agent(query: str) -> str:
    print(f"Processing query: {query}")      # captured as LOG span
    result = agent.invoke(query)
    print(f"Agent result: {result[:80]}")    # captured as LOG span
    return result

Or with the context manager:

with neatlogs.trace("my_step", capture_stdout=True):
    print("step started")
    do_work()
    print("step done")

Output is still mirrored to the real stdout — capture_stdout=True does not suppress terminal output.


What You'll See in the Dashboard

Each log call produces a LOG span as a child of the active span. The span includes:

FieldSource
Span nameneatlogs.log(): the message template. stdlib/print: the rendered message.
input.valueThe fully rendered log message
log.levelinfo, warning, error, etc.
log.{key}Each kwarg from neatlogs.log(), e.g. log.count, log.score