e-volv
Docs menu

Observer SDKs

Evolve Observer ingests logs, spans and errors from any service through one HTTP contract. Pick the SDK by where the code runs; every SDK sends the same events, so a trace that starts at the edge, passes through a Node API, fans out to a Go worker and ends in a Python agent is one trace on the graph. Languages without a native SDK send through their OpenTelemetry SDK with a preset.

Which SDK

Where the code runsUseStatus
Node.js backend — Express, NestJS, Fastify, workers, the server side of Next.js@evolve/logs (TypeScript)Available
Python service — FastAPI, Django, workers, LangChain and LangGraph agentsevolve-logs (Python)Available
Go service — net/http servers, gRPC, queue workersevolve-logs-go (Go)Available
Edge runtimes — Cloudflare Workers, Vercel Edge, Deno Deploy@evolve/logs/edgeAvailable
JavaScript agents — LangChain.js, LangGraph.js, Vercel AI SDK@evolve/logs/langchain, @evolve/logs/aiAvailable
Browser — React, Vue, plain JavaScript@evolve/logs/browserIn development; server-side capture works today
Java, .NET, Ruby, PHP, Rust — through each language’s OTel SDKOpenTelemetry presetsAvailable
Anything that already speaks OpenTelemetryOTLP/HTTP, JSON or protobufAvailable

The contract every SDK speaks

Events go to POST https://api.e-volv.io/api/public/v1/logs as { "events": [...] }; spans go to POST https://api.e-volv.io/api/public/v1/logs/otlp (OTLP/JSON) or the standard https://api.e-volv.io/api/public/v1/logs/otlp/v1/traces. Bodies may be gzipped; the Node, Python and Go SDKs gzip by default, the edge client sends identity-encoded because an isolate has no zlib.

Authentication is Authorization: Bearer evk_…, the ingest key of one project. A key belongs to exactly one project in one workspace, and the project is the service. Keys are minted on the Projects page and shown once.

A request carries at most 1 000 events or 5 MB. The ingest answers 202 when the batch is queued, 413 when it must be split, 429 when a key sends too fast (honour Retry-After), 401 for a wrong or revoked key and 402 when the workspace has no Observer plan. Throttling is per key per minute, so send batches, never one event per request. Every SDK batches at 200 events, 2 seconds or 512 KB, retries 429 and 413 on its own, drops the oldest events when the pending buffer passes twice the batch size, and counts the drops where you can read them.

What an event looks like

json
{
  "ts": "2026-09-06T10:00:00.000Z",
  "severity": 17,
  "message": "Upstream call failed: orders-db refused the connection",
  "attrs": {
    "exception.type": "ConnectionRefused",
    "exception.message": "connection refused: orders-db:5432",
    "exception.stack": "ConnectionRefused: ...\n    at ...",
    "http.route": "/api/orders"
  },
  "traceId": "069e548f7ff0a2689097afc64058de83",
  "spanId": "1f5663ada303b51f",
  "parentSpanId": "059ada4cb828c8ff"
}

severity is the OpenTelemetry number: trace 1, debug 5, info 9, warn 13, error 17, fatal 21. Three attribute conventions drive the product:

Errors. exception.type, exception.message and exception.stack on an event at severity 17 or above make it an error occurrence; the type and the normalised message form the error group. The ingest reads the stack in the language’s own grammar (V8 frames, Python tracebacks, Go runtime/debug stacks), skips vendor frames, and names the file, line and function the error came from. An error without a stack still groups, on the message alone, but has no origin to show.

Spans. span.name and durationMs on an event describe a finished span. The ingest turns it into a span row, which is how the SDKs’ span() reaches the trace graph without OTLP.

Release. deployment.environment and service.release come from init()’s environment and release options.

Attribute keys matching password|secret|token|authorization|cookie|set-cookie|api[-_]?key are redacted in the SDK before anything leaves the process, in nested objects and lists too. The list is extendable per client and never shrinks. The SDK’s own span telemetry (span.name, durationMs, token counts) is exempt on span-end events, so an llm.* span keeps its tokens.

Tracing across services

Propagation is the W3C traceparent header, 00-<traceId>-<spanId>-01. Inside a trace, the Node SDK injects it into every fetch, the Python SDK into httpx and requests once patched, and the Go SDK through its http.RoundTripper. An existing header is never overwritten. On the receiving side, the Node and Python SDKs continue the trace with runWithTraceparent and the FastAPI middleware, and the Go SDK with its net/http middleware. Queue hops carry the value in the job payload (Node, Python) or in the message’s attribute map (Go: Pub/Sub, SQS, Cloud Tasks) and continue it on the consumer, so a producer and its worker share one trace and the failure graph can walk from one to the other.

Agent integrations

Agent frameworks get one span per step, nested by run id, all in one trace, so a tool call or a model call appears inside its node on the trace graph. The Python SDK ships a LangChain and LangGraph callback handler; the Node SDK ships a LangChain.js handler (@evolve/logs/langchain) and a Vercel AI SDK middleware (@evolve/logs/ai). All three emit the same span names, so a mixed Python and JavaScript agent system reads as one vocabulary:

StepSpan name
A LangGraph nodegraph.<node>
A chain or runnablechain.<name>
A tool calltool.<name>
A model call (token counts as attrs)llm.<model>

An error in a step ends its span as failed with exception.* attributes. See the Python and Node.js pages for the two-line setup.

What differs between the runtimes

ConcernNode.jsPythonGoEdgeBrowser
Installnpm install @evolve/logspip install evolve-logsgo get github.com/Pactify-Pty-Ltd/evolve-logs-gosame npm package, /edge entrysame npm package, /browser entry
Key kindserver key evk_…, from the environment, never in a bundleserver key evk_…server key evk_…, from the environmentserver key from the runtime’s secret bindingpublic key evk_pub_…, origin-bound, write-only
Trace contextAsyncLocalStorage; withTrace(fn); flows across awaitcontextvars; with trace(); flows across await and taskscontext.Context; explicit parameter; flows where you pass itexplicit object; a child span takes its parent’s contextexplicit object, no ambient context
Spansspan(name).run(fn) or .end(err?)with span(name): — an exception ends it as failedctx, span := StartSpan(ctx, name); span.End(err)span(name, attrs, parent).run(fn) or .end(err?)span() returning a handle
Outbound propagationglobal fetch patched at init()patch_httpx(), patch_requests()Transport(base) RoundTripperspan.traceparent, set the header yourselffetch wrapper; reads traceparent from responses
Inbound propagationrunWithTraceparent(header, fn)run_with_traceparent(header); FastAPI middlewareMiddleware(handler); RunWithTraceparent(ctx, header, fn)not applicablenot applicable
Queue hopstraceparent() in the payload; runWithTraceparent on the consumertraceparent() in the payload; run_with_traceparent on the consumerInject / Extract on a string map: Pub/Sub, SQS, Cloud Tasksnot applicablenot applicable
Crash captureuncaughtException and unhandledRejection, re-emittedsys.excepthookRecover(ctx, fn): reports the panic, then re-panicsexception(err) in the handler’s catchwindow.onerror, unhandledrejection
Logger bridgespino and winston transports; NestJS LoggerServicestdlib logging handlernone yetnoneconsole capture
Agent integrationsLangChain.js handler; Vercel AI SDK middlewareLangChain / LangGraph handlernonenonenone
Flush on exitbeforeExit hook; client.flush()atexit; client.flush()background ticker; Flush(ctx) in a deferflushOnEnd(ctx) hands the flush to waitUntilsendBeacon on pagehide
Compressiongzip, alwaysgzip, alwaysgzip, alwaysidentity — no zlib in an isolateCompressionStream when available

One contract, checked

Every native SDK is held to the same wire shape by a conformance fixture: a fixed script of calls (plain logs, a redaction check, an exception, a span that succeeds, a span whose body throws, a queue hop with a pinned traceparent) whose recorded events must match byte for byte after timestamps, ids and stacks become placeholders. The Node, Python and Go SDKs run it in CI on every change; a new SDK ships only when it passes. The fixture and the stub ingest live in packages/logs-conformance.

Verifying an install

One request with the project key proves the path end to end. Within a minute the project’s Errors page shows an InstallCheck group and Tail shows the line.

shell
curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST https://api.e-volv.io/api/public/v1/logs \
  -H "authorization: Bearer $EVOLVE_LOGS_KEY" \
  -H "content-type: application/json" \
  -d '{"events":[{"ts":"2026-09-06T10:00:00Z","severity":17,"message":"install check","attrs":{"exception.type":"InstallCheck","exception.message":"ok"}}]}'

Next

Node.js and TypeScript · Python · Go · Browser, front ends and edge · Any other language: OpenTelemetry · OpenTelemetry collector