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 runs | Use | Status |
|---|---|---|
| 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 agents | evolve-logs (Python) | Available |
| Go service — net/http servers, gRPC, queue workers | evolve-logs-go (Go) | Available |
| Edge runtimes — Cloudflare Workers, Vercel Edge, Deno Deploy | @evolve/logs/edge | Available |
| JavaScript agents — LangChain.js, LangGraph.js, Vercel AI SDK | @evolve/logs/langchain, @evolve/logs/ai | Available |
| Browser — React, Vue, plain JavaScript | @evolve/logs/browser | In development; server-side capture works today |
| Java, .NET, Ruby, PHP, Rust — through each language’s OTel SDK | OpenTelemetry presets | Available |
| Anything that already speaks OpenTelemetry | OTLP/HTTP, JSON or protobuf | Available |
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
{
"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:
| Step | Span name |
|---|---|
| A LangGraph node | graph.<node> |
| A chain or runnable | chain.<name> |
| A tool call | tool.<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
| Concern | Node.js | Python | Go | Edge | Browser |
|---|---|---|---|---|---|
| Install | npm install @evolve/logs | pip install evolve-logs | go get github.com/Pactify-Pty-Ltd/evolve-logs-go | same npm package, /edge entry | same npm package, /browser entry |
| Key kind | server key evk_…, from the environment, never in a bundle | server key evk_… | server key evk_…, from the environment | server key from the runtime’s secret binding | public key evk_pub_…, origin-bound, write-only |
| Trace context | AsyncLocalStorage; withTrace(fn); flows across await | contextvars; with trace(); flows across await and tasks | context.Context; explicit parameter; flows where you pass it | explicit object; a child span takes its parent’s context | explicit object, no ambient context |
| Spans | span(name).run(fn) or .end(err?) | with span(name): — an exception ends it as failed | ctx, span := StartSpan(ctx, name); span.End(err) | span(name, attrs, parent).run(fn) or .end(err?) | span() returning a handle |
| Outbound propagation | global fetch patched at init() | patch_httpx(), patch_requests() | Transport(base) RoundTripper | span.traceparent, set the header yourself | fetch wrapper; reads traceparent from responses |
| Inbound propagation | runWithTraceparent(header, fn) | run_with_traceparent(header); FastAPI middleware | Middleware(handler); RunWithTraceparent(ctx, header, fn) | not applicable | not applicable |
| Queue hops | traceparent() in the payload; runWithTraceparent on the consumer | traceparent() in the payload; run_with_traceparent on the consumer | Inject / Extract on a string map: Pub/Sub, SQS, Cloud Tasks | not applicable | not applicable |
| Crash capture | uncaughtException and unhandledRejection, re-emitted | sys.excepthook | Recover(ctx, fn): reports the panic, then re-panics | exception(err) in the handler’s catch | window.onerror, unhandledrejection |
| Logger bridges | pino and winston transports; NestJS LoggerService | stdlib logging handler | none yet | none | console capture |
| Agent integrations | LangChain.js handler; Vercel AI SDK middleware | LangChain / LangGraph handler | none | none | none |
| Flush on exit | beforeExit hook; client.flush() | atexit; client.flush() | background ticker; Flush(ctx) in a defer | flushOnEnd(ctx) hands the flush to waitUntil | sendBeacon on pagehide |
| Compression | gzip, always | gzip, always | gzip, always | identity — no zlib in an isolate | CompressionStream 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.
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