@sol-fab/obs
v0.4.0
Published
Sol's observability naming/shape conventions for TypeScript services -- metric names and label vocabularies, Loki push shape, and W3C traceparent propagation. Not a metrics or logging library.
Maintainers
Readme
@sol-fab/obs
Sol's observability naming/shape conventions for TypeScript services, so a
TS -svc/-worker and an OCaml one land in the same Grafana panel and the
same Loki query without disagreeing on label vocabulary.
This is not a metrics or logging library — prom-client
(or any Prometheus client) and your logger of choice remain the mechanism.
This package only owns the shape: metric names, label vocabularies, the
Loki push format, and W3C traceparent propagation glue that has no
official carrier outside HTTP/gRPC.
npm install @sol-fab/obsWhat's here
metrics.ts— the exact metric names and label vocabularies fromsol-worker/sol-svc's OCaml source (sol_worker_messages_total,sol_worker_decode_errors_total,sol_svc_requests_total,sol_svc_request_duration_seconds), plus small helpers (statusClassOf,routeLabel,httpMethodLabel) that derive label values the same way the OCaml side does.tracing.ts—traceparentOf/extractTraceparent, W3Ctraceparentheader formatting/parsing for transports OpenTelemetry has no official carrier for (Kafka today; anything non-HTTP tomorrow).loki.ts—makeLokiPusher, Sol's structured-log-to-Loki push shape.identity.ts—workloadIdentity/resourceAttributes, Sol's semantic workload identity (DEC-064): the sixSOL_*values the deployment layer injects, so a TypeScript signal is scoped exactly as an OCaml one.
Workload identity
Sol renders the semantic workload identity — workspace, env, domain,
service, primitive, release — as pod labels and injects the same six values
as SOL_* environment variables (DEC-064). @sol-fab/obs composes them the way
OCaml's Sol_obs.of_env does, so the framework, not the app, owns the vocabulary:
import { makeLokiPusher, resourceAttributes } from "@sol-fab/obs";
import { resourceFromAttributes } from "@opentelemetry/resources";
const log = makeLokiPusher({ lokiUrl: process.env.LOKI_URL, service: "order-svc-ts" });
// Inside a Sol manifest the Loki stream is labelled { service: SOL_SERVICE,
// workspace, env, domain, primitive, release }, and SOL_SERVICE — the workload's
// bare Kubernetes name — wins over "order-svc-ts", so an app-pushed stream matches
// the collector-promoted one for the same pod.
const resource = resourceFromAttributes(resourceAttributes("order-svc-ts"));
// service.name = SOL_SERVICE, plus all six identity labels as resource attributes,
// so a Tempo trace is scoped exactly as the logs and metrics for the same workload.Outside a Sol manifest the variables are absent and the caller's service is used
unchanged, so a local run still labels its stream.
Why these three specific things
Each one is here because a prior hand-rolled TypeScript port of Sol's
local-demo got it wrong at least once, in a way that would silently desync a
cross-language dashboard rather than fail loudly:
- Inventing
status="decode_error"/status="db_error"label values that don't exist in the OCaml vocabulary. - Putting the raw, caller-controlled request path into the
routelabel instead of a fixed pattern — unbounded cardinality. - Hardcoding the W3C traceparent sampled flag to
"01", and separately, parsing it withparseInt(flags, 16) || 1, which treats a legitimate unsampled trace (flags=0) as sampled due to JS falsy-zero coercion.
See each module's own comments for the exact OCaml source line references.
Usage
import {
SOL_SVC_REQUESTS_TOTAL,
httpMethodLabel,
statusClassOf,
routeLabel,
traceparentOf,
makeLokiPusher,
} from "@sol-fab/obs";
import { Counter } from "prom-client";
const requests = new Counter({
name: SOL_SVC_REQUESTS_TOTAL,
help: "Total HTTP requests by method, route, and HTTP status class",
labelNames: ["method", "route", "status_class"],
});
requests.inc({
method: httpMethodLabel(req.method),
route: routeLabel(matchedRoutePattern), // undefined -> "unmatched"
status_class: statusClassOf(res.statusCode),
});
const log = makeLokiPusher({
lokiUrl: process.env.LOKI_URL,
service: "order-svc",
labels: { team: "payments" }, // low-cardinality stream labels, Sol_obs's ?context
});
log("info", "order accepted", { orderId });
// register as a shutdown hook so a line emitted just before the drain resolves
// is delivered rather than dropped with the process
await runService({ drain: () => app.close(), shutdownHooks: [() => log.flush()] });makeLokiPusher sends logs without blocking the service. Every line is written
to stdout as structured JSON as well, whether or not LOKI_URL is set, so
kubectl logs has it and a Loki outage does not lose it (OBS-048 part A). The
labels are fixed at construction and carried as Loki stream labels (Loki
requires a fixed label set), mirroring Sol_obs.of_env's ?context; flush()
awaits the pushes still in flight for a shutdown hook to drain. It reports
network and non-2xx HTTP failures to console.error with the status and up to
200 characters of the response body. It does not retry or buffer logs.
Development
npm ci
npm run build
npm testRelated
@sol-fab/kafka— Sol's Kafka policy layer; re-exports the tracing primitives from this package.- Sol — the platform these conventions come from.
License
Apache-2.0. See LICENSE. The "Sol" name and logo are trademarks and are not covered by the licence.
