Neatlogs
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