neatlogs-py-langchain
Use when adding neatlogs observability to a Python project that uses LangChain or LangGraph (imports `langchain*` / `langgraph`, builds chains, runnables, or a graph).
Files12
View on GitHubSKILL.mdskills/neatlogs-py-langchain/SKILL.md
GitHubNeatlogs Python Setup — LangChain / LangGraph
This project uses LangChain or LangGraph. Neatlogs offers two ways to instrument it:
neatlogs.langchain_handler()— a LangChain callback handler you attach per call viaconfig={"callbacks": [handler]}. This is the recommended, first-class path (LangChain's own callback system; explicit control over exactly which calls are traced). This skill uses it throughout.instrumentations=["langchain"]oninit()— zero-touch auto-instrumentation (OpenInference) that traces LangChain/LangGraph globally without editing call sites. Use this when you can't (or don't want to) thread a handler through your code.
Pick ONE — don't combine them (that double-traces). The rest of this skill shows the handler approach.
Core mechanism — neatlogs.langchain_handler()
Create ONE handler and pass it via config={"callbacks": [handler]} on the calls you want traced. The handler emits the span tree from LangChain's callback events:
CHAIN chain / graph execution
↳ LLM chat-model / llm call
↳ TOOL @tool invocation (via ToolNode)
↳ RETRIEVER retriever call
import neatlogs
from langchain_openai import ChatOpenAI
handler = neatlogs.langchain_handler()
model = ChatOpenAI(model="gpt-4o")
result = model.invoke("Hello", config={"callbacks": [handler]})
LangGraph: attach at the GRAPH invocation — NOT the per-node model call
For LangGraph, attach the handler at the graph invocation (app.invoke(...) / ainvoke / stream / astream), not on the per-node llm.invoke(). LangGraph fires the per-node on_chain_start only on the graph-level callback manager, so a handler passed to a single node's model call never sees the node boundaries — you get no node spans and the LLM span orphans to the workflow root (flat, no node hierarchy). Attach once at the graph invocation and each node gets its own span with the LLM nested under it. Do not add manual spans inside nodes.
def analyst_node(state):
response = llm.invoke(messages) # no per-node handler
return {"messages": [response]}
# ✅ attach at the graph invocation — nodes + nested LLMs all get spans
app.invoke(state, config={"callbacks": [handler]}) # graph level ✅
# NOT: llm.invoke(messages, config={"callbacks": [handler]}) # per-node ❌ (no node spans, LLM orphans)
The same boundary rule applies asynchronously. An async def node should await
llm.ainvoke(...) without a per-node callback; attach the handler once to
await app.ainvoke(..., config={"callbacks": [handler]}) or
app.astream(..., config={"callbacks": [handler]}). Do not call synchronous
invoke() from an async node, and do not await app.astream(...) itself—iterate
it with async for.
(Plain LangChain — LCEL chains / bare model.invoke() — is the opposite: attach per model/chain call as in the example above. The graph-level rule is LangGraph-specific.)
Deep Agents (deepagents) — prebuilt harness, attach on the top-level invoke
Deep Agents (from deepagents import create_deep_agent) is a LangGraph harness — you do NOT write the nodes, so there's no per-node model call to attach to. Pass the handler on the agent's top-level invoke / ainvoke / stream:
handler = neatlogs.langchain_handler()
agent = create_deep_agent(model="openai:gpt-4o-mini", tools=[...], system_prompt="...")
result = agent.invoke(
{"messages": [{"role": "user", "content": "..."}]},
config={"callbacks": [handler]},
)
For hand-written LangGraph (your own nodes) as well as prebuilt harnesses (Deep Agents), the top-level / graph invocation is the attach point — use it. (Consistent with the graph-level rule above.)
Use a WORKFLOW span and neatlogs.log only for your own surrounding orchestration; the handler-owned spans nest underneath. Never add manual LLM, node, chain, tool, or retriever spans for operations the handler captures.
What the handler captures (DO NOT manually decorate)
- Chat-model / LLM calls → LLM span (model, tokens, latency)
@toolinvocations via ToolNode → TOOL span- Retriever calls → RETRIEVER span
- Chain / node execution → CHAIN span
Token usage is read from both LangChain's standard usage_metadata and provider llm_output.token_usage, normalized to neatlogs.llm.token_count.* across providers — including Gemini (prompt_token_count / candidates_token_count), cache tokens, and reasoning tokens. Async callbacks (ainvoke / astream) are supported, and LangGraph node spans use the bare node name (e.g. researcher). Nothing to configure.
Steps
- Install SDK →
references/1-install-sdk.md - Add init() →
references/2-add-init.md - Set environment variables →
references/3-set-env.md - Create + attach the callback handler →
references/4-attach-handler.md - Optionally add an app-owned WORKFLOW for real surrounding orchestration →
references/5-add-workflow.md - Verify handler-owned LLM calls are not manually wrapped →
references/6-wrap-llm-calls.md - Verify tool functions are untouched →
references/7-verify-tools.md7.5. Embeddings: decorate ONLY custom ones →references/7.5-embeddings.md - Add flush/shutdown →
references/8-flush-shutdown.md
Rules (apply to ALL steps)
neatlogs.init()MUST execute BEFORE any LangChain library imports.- If
load_dotenv()exists, it MUST run BEFOREneatlogs.init(). - This skill uses the callback handler; if you go that route, do NOT ALSO pass
instrumentations=["langchain"]toinit()— running both double-traces. (instrumentations=["langchain"]is a valid standalone alternative — see the intro — just don't combine the two. Provider instrumentors for embeddings are a separate concern — see step 7.5.) - Create ONE
neatlogs.langchain_handler()and pass it viaconfig={"callbacks": [handler]}. For plain LangChain (LCEL chains / bare model calls) attach per model/chain call. For LangGraph attach at the graph invocation (app.invoke(..., config={"callbacks": [handler]})), NOT the per-nodellm.invoke(). - Never hardcode API keys in source. Use
os.getenv(). - For EU projects, set
endpoint="https://eu.ingest.neatlogs.com"inneatlogs.init(); for other managed projects, omitendpointandNEATLOGS_ENDPOINT. - Add imports ONLY for what a file actually uses:
- File calls
neatlogs.langchain_handler(...)/neatlogs.span(...)/neatlogs.log(...)→ addimport neatlogs.
- File calls
- When present,
import neatlogsgoes at module top level, never inside functions. @neatlogs.span()goes BELOW framework decorators, closest todef.- Minimal edits only. Add the handler + decorators + imports. Do not reformat or refactor.
- NEVER add
@neatlogs.span()to@toolfunctions or LangGraph node functions. - The handler self-roots a parentless supported call. Add at most one app-owned
@span(kind="WORKFLOW")on a user-facing entry only when it performs meaningful pre/post work or coordinates multiple calls.@span(kind="EMBEDDING")is added only for a custom embedder (step 7.5). - NEVER put
neatlogs.trace(kind="LLM"),@neatlogs.span, or another provider instrumentor aroundinvoke/stream, graph nodes, tools, or retrievers captured by the handler.
Safety gate
Before any edit, confirm this service is Python. Identify the active interpreter and package manager from its manifests and lockfiles, then read the declared and installed SDK version. Do not install or change dependencies during this inspection. Run Doctor through the active interpreter so it can use only the installed SDK:
python -m neatlogs doctor --local --json
Do not substitute npx, uvx, pipx run, a Wizard command, or another
downloaded Doctor. Local mode must be read-only and network-free. It requires no
credential and must not change source or configuration. Require
format_version: "neatlogs.doctor/v2", runtime.language: "python", and
runtime.schema_version: "2". Treat runtime.sdk_version as evidence of the
installed package, not as an exact-version allowlist.
If the command is missing or its result has the wrong format, language, or schema, fail closed. Check the canonical package registry for the latest published stable release. If the project uses an older release, show the exact upgrade command for the detected package manager and obtain explicit user approval before running it. Accept newer compatible releases and never downgrade one. If the installed release is already current but lacks Doctor v2, stop and give safe manual/support remediation. Rerun local Doctor after any approved upgrade. Do not edit while this gate is unresolved.
A local pass proves only that the installed SDK produced and validated its
controlled in-process envelope. It does not prove that the application is
instrumented, that anything was exported, or that a hosted trace is visible.
Preserve every reason_code and remediation_code exactly. Treat a warning,
failure, or unknown future code as manual/support remediation unless the code is
explicitly safe/fixable here. The only source fixes allowed by this gate are:
INSTRUMENTOR_INACTIVE: apply only this skill's documented initialization or wrapper step.ROOT_MISSING: add only the already-requested, documented WORKFLOW boundary at a confirmed entry point.ROOT_NOT_ENDED: add only this skill's documented lifecycle hook.
Do not edit for credential, authentication, transport, backend, ambiguous ownership, or unknown codes. Never reproduce backend PII, routing, mapping, or finalization implementation. Before any build, test, or user-workflow command, show the exact command and obtain explicit user approval. Make reruns idempotent: reread the target first and never duplicate initialization, wrappers, roots, or shutdown hooks. Keep a pre-edit diff. If an approved check fails, use the rollback plan to revert only the edits from this run when they can be isolated safely. Otherwise, stop and give manual recovery instructions that preserve unrelated user work.
After instrumentation, obtain approval for the project checks and one representative real workflow. Obtain separate approval for the authenticated probe. Use only a credential already supplied through the process environment. Never print it, place it in command arguments or files, copy it into output, or put it in agent context.
python -m neatlogs doctor --probe --json
Probe mode sends one controlled four-span trace through POST /v1/traces with
x-neatlogs-doctor: v1, then reads that exact trace through
GET /api/traces/v3/{trace_id} with the same project credential. Accept a
probe pass only when capture and readback trace IDs match, the trace is
finalized, exactly four spans contain one meaningful WORKFLOW root with
AGENT→LLM and root→TOOL relationships, there are no duplicates, required
semantics and I/O are present, and token values remain numeric. Never infer
success from installation, local logs, exporter flush, HTTP 2xx, or any
uncorrelated trace. Probe success proves the controlled path only. Verify the
real user workflow separately through the completion gate below.
Completion gate
After local Doctor passes and the requested instrumentation is in place:
- Show the exact project build, test, and real-workflow commands and obtain explicit user approval before running them.
- Run only the approved checks. Restart a long-running process so it loads the new initialization and wrappers; keep reruns idempotent.
- Exercise one representative real user workflow. End every opened span and use the documented flush/shutdown lifecycle for that process type.
- Through the target project's normal product trace view or supported public read path, verify that exact run is finalized, has one meaningful root and the expected semantic hierarchy, and contains no duplicate operation spans.
Keep project credentials in the process environment or client secret storage; never put them in commands, output, files, or agent context. Do not use a legacy marker-discovery protocol. Installation, local logs, exporter flush, HTTP 2xx, a local Doctor pass, and a separate probe pass are not proof that the application's real workflow is correct. If the exact user trace cannot be inspected, report the missing access or observation as a blocker and provide rollback/manual recovery instructions without claiming completion.
Reference
- Span kinds →
references/span-kinds.md - Sessions & end-users (per-turn
identify()for customer analytics) →references/sessions-and-end-users.md