Skip to content

Zero-cost, structured, span-based tracing framework for Nim — inspired by Rust's tracing crate. Pure stdlib, no external deps.

License

Notifications You must be signed in to change notification settings

copyleftdev/nimtrace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nimtrace

A structured, span-based tracing framework for Nim — inspired by Rust's tracing crate.

nimtrace provides structured diagnostics with spans (time intervals) and events (points in time), enabling rich, contextual logging with zero-cost abstractions when tracing is disabled.

Features

  • Structured tracing — spans and events with typed fields, not just string messages
  • Zero-cost when disabled — compile with -d:nimtraceDisabled to eliminate all tracing code
  • Thread-safe — thread-local span stacks, no locks on the hot path
  • Pluggable subscribers — runtime-polymorphic output backends
  • Built-in subscribers — human-readable (colored), JSON Lines, and no-op
  • Environment-based filteringNIMTRACE_LOG=warn,mymod=debug syntax
  • Auto-instrumentation{.traced.} pragma wraps procs in spans automatically
  • OpenTelemetry-compatible IDs — 8-byte SpanId, 16-byte TraceId
  • Pure Nim — no external C dependencies, stdlib only

Quick Start

import nimtrace

# Set up a subscriber at program startup
let subscriber = newFmtSubscriber(minLevel = lvlInfo)
setGlobalSubscriber(subscriber)

# Emit events
info("application started")
debug("this is filtered out")
warn("disk usage high", @[field("percent", 92)])

# Create spans
infoSpan "handle_request", @[field("method", "GET"), field("path", "/api")]:
  info("processing request")
  debugSpan "db_query":
    info("query complete", @[field("rows", 42)])

Output:

2025-02-18T04:50:00Z  INFO handle_request{method="GET" path="/api"}: processing request
2025-02-18T04:50:00Z  INFO handle_request{method="GET" path="/api"}:db_query: query complete rows=42
2025-02-18T04:50:00Z  INFO handle_request{method="GET" path="/api"}: closed (1.2ms)

Installation

nimble install nimtrace

Or add to your .nimble file:

requires "nimtrace >= 0.1.0"

API Reference

Event Macros

trace("verbose detail")
debug("debugging info")
info("normal operation")
warn("something unusual")
error("something failed")

# With structured fields
info("request handled", @[field("status", 200), field("latency_ms", 12.5)])

Span Macros

infoSpan "operation_name":
  # code runs inside the span
  info("event within span")

# With fields
infoSpan "http_request", @[field("method", "POST"), field("url", "/api")]:
  discard

Available at all levels: traceSpan, debugSpan, infoSpan, warnSpan, errorSpan.

Auto-Instrumentation

import nimtrace
import nimtrace/instrument

proc handleRequest(method: string, path: string): int {.traced.} =
  # Automatically wrapped in a span named "handleRequest"
  # with fields: method="..." path="..."
  result = 200

Async Context Propagation

When using async/await, multiple coroutines share the same thread-local span stack. nimtrace provides primitives to preserve context across yield points:

import nimtrace

proc handler() {.async.} =
  infoSpan "handle_request":
    info("before await")

    # Preserve context across await
    preserveContext:
      await someAsyncOperation()

    info("after await — still in the right span")

For spawning child async tasks that inherit parent context:

proc parentTask() {.async.} =
  infoSpan "parent":
    let childCtx = forkContext()
    await childTask(childCtx)

proc childTask(ctx: SpanContextSnapshot) {.async.} =
  withContext ctx:
    infoSpan "child_work":
      info("running with parent's span ancestry")

Key APIs:

  • captureContext() — snapshot the current span stack
  • restoreContext(snapshot) — replace the span stack with a snapshot
  • preserveContext: body — auto-save/restore around a block (e.g., containing await)
  • withContext snapshot: body — run body within a captured context (scoped)
  • forkContext() — copy context for a child task

Subscribers

FmtSubscriber (Human-Readable)

let sub = newFmtSubscriber(
  minLevel = lvlInfo,    # filter threshold
  useColors = true,      # ANSI colors (default: true)
  stream = stderr        # output stream (default: stderr)
)
setGlobalSubscriber(sub)

JsonSubscriber (Machine-Readable)

let sub = newJsonSubscriber(
  minLevel = lvlDebug,
  stream = stdout
)
setGlobalSubscriber(sub)

Outputs one JSON object per line (JSON Lines format):

{"timestamp":"2025-02-18T04:50:00Z","level":"INFO","name":"request handled","fields":{"status":200}}

Environment Filter

# Reads NIMTRACE_LOG env var: "warn,mymod=debug,db=trace"
let inner = newFmtSubscriber()
let sub = newEnvFilter(inner, "NIMTRACE_LOG")
setGlobalSubscriber(sub)

Format: default_level,module1=level1,module2=level2

OtlpSubscriber (OpenTelemetry Export)

Export spans to any OpenTelemetry-compatible collector (Jaeger, Grafana Tempo, Honeycomb, etc.) via OTLP/HTTP JSON:

let sub = newOtlpSubscriber(
  endpoint = "http://localhost:4318/v1/traces",
  serviceName = "my-service",
  resourceAttrs = @[field("deployment.environment", "production")],
  headers = @[("Authorization", "Bearer my-token")],
  batchSize = 64         # auto-flush after 64 completed spans
)
setGlobalSubscriber(sub)

# ... application code ...

# Flush remaining spans at shutdown
sub.flush()

Features:

  • Batch export — collects completed spans, flushes on threshold or explicit flush()
  • Span events — nimtrace events within a span become OTLP span events
  • Resource attributesservice.name + custom attributes
  • Status codes — error-level spans get STATUS_CODE_ERROR
  • Custom headers — for authentication with hosted collectors
  • Failed export retry — re-queues batch on HTTP failure

Works with any OTLP/HTTP JSON endpoint:

  • Jaeger: http://localhost:4318/v1/traces
  • Grafana Tempo: http://localhost:4318/v1/traces
  • Honeycomb: https://api.honeycomb.io/v1/traces with API key header

Compile-Time Configuration

Flag Effect
-d:nimtraceDisabled Eliminate all tracing code at compile time
-d:nimtraceMaxLevel:warn Remove trace/debug/info at compile time

Fields

Fields are typed key-value pairs:

field("user", "alice")       # string
field("count", 42)           # integer
field("ratio", 3.14)         # float
field("active", true)        # boolean

Architecture

nimtrace
├── core        # Level, Field, Metadata, SpanId, TraceId
├── span        # Span, SpanContext, thread-local stack
├── event       # Event type
├── subscriber  # SubscriberBase (virtual methods)
├── registry    # Global subscriber management
├── api         # Event/span templates (user-facing)
├── filter        # LevelFilter, EnvFilter
├── instrument    # {.traced.} pragma macro
├── async_context # Async-aware context propagation
└── subscribers
    ├── fmt     # Colored, human-readable output
    ├── json    # JSON Lines output
    ├── otlp    # OpenTelemetry OTLP/HTTP JSON export
    └── noop    # Zero-cost no-op (default)

Performance

Benchmarks on Nim 2.2.6, -d:release --opt:speed:

Operation Time
Disabled path (no subscriber) ~15ns
Field creation (int) ~5ns
SpanId generation ~5ns
Level filter check ~40ns
Event emission (simple) ~63ns
Event emission (3 fields) ~119ns
Span lifecycle (simple) ~157ns
Span lifecycle (with fields) ~222ns
Nested spans (2 levels) ~299ns

With -d:nimtraceDisabled, all tracing compiles to nothing (0ns).

Testing

# Run all tests
for t in tests/test_*.nim; do nim c --hints:off --path:src -r $t; done

# Run with ORC
for t in tests/test_*.nim; do nim c --hints:off --path:src --mm:orc -r $t; done

# Run benchmarks
nim c --hints:off --path:src -d:release --opt:speed -r benchmarks/bench_hot_path.nim

Design Principles

  • Zero-cost abstractions — templates inline everything; disabled paths compile away
  • Value-type spans — stack allocated, no GC pressure on the hot path
  • Thread-local stacks{.threadvar.} for lock-free span context propagation
  • Composable subscribers — filters wrap subscribers, build any pipeline
  • No silent failure — explicit error handling, no swallowed exceptions

License

MIT

About

Zero-cost, structured, span-based tracing framework for Nim — inspired by Rust's tracing crate. Pure stdlib, no external deps.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages