Skip to content

Latest commit

 

History

History
143 lines (104 loc) · 9.57 KB

metrics.md

File metadata and controls

143 lines (104 loc) · 9.57 KB

Framework-neutral metrics

See the data policy for classifications, rejections, and label limits.

Use @equipe-tech/observability/metrics from NestJS, plain Node.js, or any other JavaScript runtime. The facade exports counters, histograms, observable gauges, bounded flush, and idempotent close without exposing Effect types.

import { createMetrics } from "@equipe-tech/observability/metrics";

const metrics = await createMetrics({
  serviceName: "checkout-api",
  serviceVersion: "1.4.0",
  environment: "production",
  deploymentEnvironmentAlias: "omitted",
  otlpEndpoint: "http://localhost:4318",
});

const orders = metrics.counter({
  name: "orders.created",
  description: "Created orders",
  unit: "1",
});

const duration = metrics.histogram({
  name: "orders.duration",
  description: "Order duration",
  unit: "ms",
  boundaries: [10, 25, 50, 100, 250, 500],
});

const queue = metrics.observableGauge(
  {
    name: "orders.queue.depth",
    description: "Pending order count",
    unit: "1",
  },
  () => [{ value: 4, attributes: [{ key: "deployment.region", value: "south" }] }],
);

orders.add(1, [{ key: "deployment.region", value: "south" }]);
duration.record(42, [{ key: "deployment.region", value: "south" }]);
await metrics.flush();
queue.unregister();
await metrics.close();

Contract-bound producer

Application code should define metrics in defineTelemetryContract and bind the compiled contract to the existing facade. makeMetricProducer is exported from the package root and does not expose Effect types.

import { Effect } from "effect";
import { defineTelemetryContract, makeMetricProducer } from "@equipe-tech/observability";

const contract = await Effect.runPromise(
  defineTelemetryContract({
    version: 1,
    events: {},
    metrics: {
      OrdersCreated: {
        name: "orders.created",
        description: "Created orders",
        unit: "1",
        kind: "counter",
        attributes: {
          "order.channel": {
            classification: "public",
            allowedValues: ["web", "mobile"],
            maximumCardinality: 2,
          },
        },
      },
    },
    auditActions: {},
  }),
);

const producer = makeMetricProducer(contract, observability.metrics);
producer.counter("OrdersCreated").add(1, { "order.channel": "web" });

The producer checks contract shape and allowed values, then probes declared cardinality. Each declared cardinality budget belongs to one compiled metric definition. Separate compiled contracts receive separate declared budgets even when they use the same metric name. Runtime instrument, attribute, and series limits remain global to the shared runtime. The existing metrics facade applies data policy and runtime limits. The producer commits declared cardinality only after the facade accepts the record. A rejected record does not consume declared cardinality. The shared runtime owns the declared cardinality catalog, so producers and facade leases with equal options share it. Enabled Node observability handles expose the pool-matched facade through metrics. Disabled handles run contract validation but export nothing. Their facade uses the disabled policy defaults rather than the enabled handle's configured evlog policy.

InvalidMetricMeasurement reports stable OBS_METRIC_* codes for unknown aliases, kind mismatches, missing or undeclared attributes, closed-set violations, invalid values, and declared cardinality overflow. Gauge callback failures use CONTRACT_REJECTED and expose the evidence-safe code through contractReason.

createMetrics remains available for framework integration and low-level runtime use. It does not require a telemetry contract.

Configuration

createMetrics parses identity before acquiring a runtime. serviceName uses lowercase letters, numbers, and single hyphens between segments, with at most 63 characters. environment uses the same grammar with at most 32 characters. serviceVersion accepts SemVer 2.0.0 or a 7 to 64 character lowercase hexadecimal immutable release identifier. Metrics omit service.instance.id from resources.

deploymentEnvironmentAlias accepts omitted or emitted and defaults to omitted. otlpEndpoint must be an HTTP or HTTPS URL without credentials. exportIntervalMilliseconds and flushTimeoutMilliseconds must be positive safe integers when supplied. enabled accepts a boolean and defaults to true.

An identity failure throws MetricsError with code INVALID_CONFIGURATION. Its field and rule properties identify the rejected identity field and its exact grammar.

Lifecycle

createMetrics validates the complete configuration before acquiring a runtime lease. Equal active configurations share one runtime, registry, periodic exporter, and lifecycle queue. They also share exactly one exporter when facade and layer leases overlap. The most recently acquired active layer binding provides the transport; the built-in fetch transport is the fallback when no layer binding is active. Transport identity is deliberately excluded from the pool key because including it would create parallel runtimes and duplicate exporters. exportIntervalMilliseconds controls periodic collection and defaults to 10,000 milliseconds.

add, record, gauge registration, and unregister are synchronous. flush and close are asynchronous and bounded by flushTimeoutMilliseconds, which defaults to 3 seconds. A repeated close returns the same promise. Recording, registration, or flushing after close throws MetricsError with code CLOSED. Cumulative counter and histogram values remain in the shared runtime after an individual lease closes and retain the runtime start time.

Observable callbacks run synchronously immediately before periodic, manual, and final exports. One callback may return up to 100 observations. Failures omit that gauge from the current export and appear in FlushResult.gaugeFailures without suppressing valid instruments. Unregistering the final callback removes the gauge from later exports.

Set enabled: false to validate configuration, instrument definitions, measurement values, and measurement attributes while returning no-op handles without an exporter, timer, callback invocation, request, or retained runtime lease. Disabled mode cannot report callback-result failures, definition conflicts, or active-runtime cardinality limits because it retains no catalog, callbacks, or series.

Instrument identity

The metric name is the instrument identity within one runtime. Repeating the exact kind, description, unit, and histogram boundaries is compatible. A facade definition mismatch throws INSTRUMENT_CONFLICT before creating a partial instrument. A name collision discovered later against a direct Effect metric rejects export with non-retryable EXPORT_FAILED and sends no metrics request.

Names use [A-Za-z][A-Za-z0-9_.\-/]{0,254}. Units are 1, %, or a case-sensitive ASCII unit expression such as ms, By/s, or m/s^2. Descriptions contain 1 to 1,024 characters without control characters. Histograms require 1 to 50 finite, strictly increasing boundaries.

Counters accept finite additions greater than or equal to zero. Histograms accept any finite observation.

Attributes and cardinality

Pass attributes as an array of { key, value } items. Values are strings, finite numbers, or booleans. Keys use at least two lowercase dotted segments and contain at most 128 characters. Duplicate keys and the reserved identifiers unit, time_unit, service.instance.id, trace.id, span.id, user.id, and session.id are rejected. Instance, trace, span, user, and session identifiers never become metric labels. A measurement accepts at most 16 attributes. String values contain 1 to 64 characters, use the label grammar [A-Za-z0-9/][A-Za-z0-9._:/-]{0,63}, and cannot have identifier shapes such as UUID, trace ID, span ID, ULID, or a numeric identifier of at least seven digits. A value is also rejected with the evidence-safe reason blocked-value when blocked-value or structured-assignment policy would transform it. Templated routes such as /orders/:id remain valid labels.

The runtime enforces these lifetime limits:

  • 100 facade instrument names per runtime lifetime
  • 100 distinct values per label per instrument lifetime
  • 1,000 series identities per instrument
  • 10,000 series identities per runtime
  • 16 callbacks per observable gauge
  • 100 observations per callback collection

Series identity sorts attribute keys and preserves scalar types, so string "1", number 1, and boolean true remain distinct. Cardinality limits do not reset after gauge unregister.

Failures

Synchronous validation and registration failures throw MetricsError with one of these codes:

  • INVALID_CONFIGURATION
  • INVALID_INSTRUMENT
  • INVALID_MEASUREMENT
  • INSTRUMENT_CONFLICT
  • LIMIT_EXCEEDED
  • POLICY_BLOCKED
  • CLOSED

POLICY_BLOCKED reports a dotted-key, reserved-identifier, classification, blocked-value, string-bound, or identifier-shape rejection without returning the rejected key or value. LIMIT_EXCEEDED reports the 16-label measurement bound or a lifetime cardinality bound, including the 100-values-per-label-per-instrument limit.

flush and the final export from close reject with EXPORT_FAILED or FLUSH_TIMED_OUT. Transport failures and timeouts are retryable on an open lifecycle. Definition and name-conflict export failures are not retryable until the conflicting instrument is renamed or aligned. A direct Effect Metric datapoint carrying service.instance.id also fails with non-retryable EXPORT_FAILED; remove the reserved key before another export. close releases the runtime lease even when its final export rejects.