4e7e568525
- Bump version to 1.1.0 across all components - Remove debug CI artifacts (debug.yml, test_all.nim.bak, test_lock) - Update CHANGELOG with v1.1.0 release notes - Bug fixes: irNeg, distributed txn, sharding, Raft majority, MVCC, LSM-Tree, auth (JWT + SCRAM-SHA-256), wire protocol, SQL injection, ReDoS, graph, vector, FTS, build warnings - Client SDKs: JS/TS, Python, Nim, Rust with tests and examples - Docker: production + source + test compose configs - TLA+: crossmodal, backup, recovery specs with symmetry reduction
133 lines
3.8 KiB
Nim
133 lines
3.8 KiB
Nim
## Tracing — lightweight OpenTelemetry-compatible span recording
|
|
import std/json
|
|
import std/times
|
|
import std/monotimes
|
|
import std/tables
|
|
import std/httpclient
|
|
import std/strutils
|
|
|
|
type
|
|
SpanStatus* = enum
|
|
ssOk = "OK"
|
|
ssError = "ERROR"
|
|
|
|
Span* = ref object
|
|
traceId*: string
|
|
spanId*: string
|
|
parentSpanId*: string
|
|
name*: string
|
|
startTime*: int64 # monotonic ticks (for duration)
|
|
startTimeEpochNs*: int64 # Unix epoch nanoseconds (for OTLP)
|
|
endTime*: int64
|
|
endTimeEpochNs*: int64
|
|
durationMs*: float64
|
|
status*: SpanStatus
|
|
attributes*: Table[string, string]
|
|
|
|
Tracer* = ref object
|
|
spans*: seq[Span]
|
|
activeSpan*: Span
|
|
enabled*: bool
|
|
nextId*: uint64
|
|
|
|
var defaultTracer* = Tracer(spans: @[], activeSpan: nil, enabled: false, nextId: 1)
|
|
|
|
proc genId(tracer: Tracer): string =
|
|
result = $tracer.nextId
|
|
inc tracer.nextId
|
|
|
|
proc beginSpan*(tracer: Tracer, name: string, attributes: Table[string, string] = initTable[string, string]()): Span =
|
|
if not tracer.enabled: return nil
|
|
let nowNs = int64(epochTime() * 1_000_000_000)
|
|
let span = Span(
|
|
traceId: if tracer.activeSpan != nil: tracer.activeSpan.traceId else: genId(tracer),
|
|
spanId: genId(tracer),
|
|
parentSpanId: if tracer.activeSpan != nil: tracer.activeSpan.spanId else: "",
|
|
name: name,
|
|
startTime: getMonoTime().ticks(),
|
|
startTimeEpochNs: nowNs,
|
|
attributes: attributes,
|
|
)
|
|
tracer.spans.add(span)
|
|
tracer.activeSpan = span
|
|
return span
|
|
|
|
proc endSpan*(tracer: Tracer, span: Span, status: SpanStatus = ssOk) =
|
|
if span == nil or not tracer.enabled: return
|
|
span.endTime = getMonoTime().ticks()
|
|
span.endTimeEpochNs = int64(epochTime() * 1_000_000_000)
|
|
span.durationMs = float64(span.endTime - span.startTime) / 1_000_000.0
|
|
span.status = status
|
|
if span.parentSpanId.len > 0:
|
|
for s in tracer.spans:
|
|
if s.spanId == span.parentSpanId:
|
|
tracer.activeSpan = s
|
|
return
|
|
tracer.activeSpan = nil
|
|
|
|
proc setSpanError*(span: Span, msg: string) =
|
|
if span == nil: return
|
|
span.status = ssError
|
|
span.attributes["error.message"] = msg
|
|
|
|
proc flushSpans*(tracer: Tracer): JsonNode =
|
|
result = newJArray()
|
|
for span in tracer.spans:
|
|
result.add(%*{
|
|
"traceId": span.traceId,
|
|
"spanId": span.spanId,
|
|
"parentSpanId": span.parentSpanId,
|
|
"name": span.name,
|
|
"durationMs": span.durationMs,
|
|
"status": $span.status,
|
|
"attributes": span.attributes,
|
|
})
|
|
tracer.spans = @[]
|
|
|
|
proc enable*(tracer: Tracer) =
|
|
tracer.enabled = true
|
|
|
|
proc disable*(tracer: Tracer) =
|
|
tracer.enabled = false
|
|
|
|
proc exportOtlp*(tracer: Tracer, endpoint: string = "http://localhost:4318/v1/traces"): bool =
|
|
## Export spans via OTLP/HTTP (JSON format).
|
|
## Returns true on success.
|
|
if tracer.spans.len == 0: return true
|
|
var otlpSpans = newJArray()
|
|
for span in tracer.spans:
|
|
var attrs = newJArray()
|
|
for k, v in span.attributes:
|
|
attrs.add(%*{"key": k, "value": {"stringValue": v}})
|
|
otlpSpans.add(%*{
|
|
"traceId": span.traceId,
|
|
"spanId": span.spanId,
|
|
"parentSpanId": span.parentSpanId,
|
|
"name": span.name,
|
|
"kind": "SPAN_KIND_INTERNAL",
|
|
"startTimeUnixNano": $span.startTimeEpochNs,
|
|
"endTimeUnixNano": $span.endTimeEpochNs,
|
|
"status": {"code": if span.status == ssOk: "STATUS_CODE_OK" else: "STATUS_CODE_ERROR"},
|
|
"attributes": attrs,
|
|
})
|
|
let body = %*{
|
|
"resourceSpans": [{
|
|
"resource": {"attributes": [
|
|
{"key": "service.name", "value": {"stringValue": "baradadb"}}
|
|
]},
|
|
"scopeSpans": [{
|
|
"scope": {"name": "baradadb-tracer", "version": "1.1.0"},
|
|
"spans": otlpSpans
|
|
}]
|
|
}]
|
|
}
|
|
try:
|
|
let client = newHttpClient()
|
|
client.headers["Content-Type"] = "application/json"
|
|
discard client.postContent(endpoint, body = $body)
|
|
client.close()
|
|
tracer.spans = @[]
|
|
return true
|
|
except:
|
|
return false
|