@ultimat3/core 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,12 +11,18 @@ Zero dependencies, zero `@ultimat3/*` imports.
11
11
  | request context on `AsyncLocalStorage` | `context.ts` |
12
12
  | `Actor` (`user \| service \| agent \| anonymous`) | `actor.ts` |
13
13
  | typed env validated at boot | `env.ts` |
14
+ | `.env.example` rendered from that schema, and its drift check | `env-example.ts` |
15
+ | named environments + `ULTIMATE_ENV` resolution | `environment.ts` |
16
+ | a value that cannot be printed by accident | `secret.ts` |
14
17
  | `defineConfig()` for `app.config.ts` | `config.ts` |
15
18
  | runtime roles + `ROLE` resolution | `roles.ts` |
16
19
  | `Clock` — the only source of "now" | `clock.ts` |
17
20
  | UUIDv7, nanoid, branded ids | `ids.ts` |
18
21
  | structured JSON logging + redaction | `logger.ts` |
19
22
  | OTel-shaped spans, always on, no-op by default | `telemetry.ts` |
23
+ | OTel-shaped counter / gauge / histogram, same seam | `metrics.ts` |
24
+ | the `/metrics` scrape body | `metrics-text.ts` |
25
+ | the series every process emits, incl. what the chart scales on | `runtime-metrics.ts` |
20
26
  | graceful drain, `/healthz`, `/readyz` | `lifecycle.ts` |
21
27
  | the sockets this process opened, so a self-request is not egress | `listeners.ts` |
22
28
  | `defineService('orgs', …)` → `ctx.orgs`, rebuilt per actor | `service.ts` |
@@ -90,7 +96,7 @@ registered survives an actor swap unrebuilt.
90
96
  export const env = defineEnv({
91
97
  DATABASE_URL: { type: 'url', secret: true },
92
98
  PORT: { type: 'port', default: 3000 },
93
- STAGE: { type: 'enum', values: ['dev', 'staging', 'prod'] },
99
+ REGION: { type: 'enum', values: ['us', 'eu'] },
94
100
  SENTRY_DSN: { type: 'url', required: false },
95
101
  NATS_URL: { type: 'url', role: 'sync' }, // only required for ROLE=sync
96
102
  });
@@ -99,7 +105,46 @@ export const env = defineEnv({
99
105
  Every missing or malformed key is listed in one `X_ENV_MISSING`. `secret: true` keys are
100
106
  redacted in logs and masked in `checkEnv()` output; `describeEnv()` emits declarations only,
101
107
  safe for `x.manifest.json`. Omit `required` for required — `required: false` is the only
102
- loosening.
108
+ loosening. Never declare an env var for *which deploy this is* — that is `ULTIMATE_ENV`, below.
109
+
110
+ `.env.example` is a **projection** of that schema, never a second list:
111
+ `renderEnvExample(schema)` writes it, `assertEnvExample(schema, text)` fails with
112
+ `X_ENV_EXAMPLE_DRIFT` when a declared key has no line — the failure that otherwise arrives as
113
+ somebody else's `X_ENV_MISSING` on a variable nobody documented.
114
+
115
+ Loading `.env` is **Bun's**, not ours. `envFileCandidates()` states what it does, measured:
116
+ `.env` → `.env.<mode>` → `.env.local`, with `.env.local` skipped under test, and the mode being
117
+ `production`, `test` or **`development` for everything else — `staging` included**. There is no
118
+ `.env.staging`; a staging deploy carries real environment variables.
119
+
120
+ ## One environment, one key
121
+
122
+ ```ts
123
+ resolveEnvironment(); // 'development' | 'test' | 'staging' | 'production'
124
+ isProduction(); // exact; nothing else counts
125
+ isLocal(); // development or test — never staging
126
+ ```
127
+
128
+ `ULTIMATE_ENV` is the key, `NODE_ENV` the fallback (platforms already set it). Values are
129
+ `NODE_ENV`'s spellings plus `staging` — `prod` and `dev` are typos, not aliases, and
130
+ `ULTIMATE_ENV=prod` is `X_ENVIRONMENT_INVALID`. An unrecognised `NODE_ENV` is *not* an error: it
131
+ is not our key. This is the twin of `roles.ts` — `ROLE` says what the process does,
132
+ `ULTIMATE_ENV` says which deploy it belongs to.
133
+
134
+ ## A secret is redacted by value, not by name
135
+
136
+ ```ts
137
+ const dsn = secret(process.env.DATABASE_URL ?? '', 'DATABASE_URL');
138
+ logger.info('boot', { dsn }); // {"dsn":"[redacted]"}
139
+ connect(revealSecret(dsn)); // the one greppable way out
140
+ ```
141
+
142
+ `redactKeys()` catches a secret travelling under a name someone remembered to list. A `Secret`
143
+ box catches the other case: `String()`, template literals, `+`, `JSON.stringify`, `console.log`,
144
+ the logger and an error's `meta` all render `[redacted]`, whatever key it sits under. It is
145
+ frozen and everything but `label` is non-enumerable, so `{ ...dsn }` cannot spread the value back
146
+ out. There is no vault integration and there will not be one — that is a platform primitive
147
+ (axiom 7); a `Secret` plus the platform's own secret store is the whole design.
103
148
 
104
149
  ## Time, ids, telemetry, drain
105
150
 
@@ -109,12 +154,53 @@ loosening.
109
154
  - `withSpan('action.publishPost', fn)` is free until `configureTelemetry({ exporter })`.
110
155
  Traces cross process boundaries via `traceparent()` / `parseTraceparent()` — Sentry, Honeycomb
111
156
  and OTLP all plug in as a `SpanExporter`.
157
+ - Metrics are the same shape one signal over: `counter()`, `gauge()`, `histogram()`, aggregated
158
+ in process, free until `configureMetrics({ exporter })`. See below.
112
159
  - `onShutdown(name, hook, { phase })` with phases `accept → inflight → close` under one
113
160
  deadline; `readyzPayload()` flips to 503 the moment draining starts, `healthzPayload()` stays
114
161
  200 until stopped.
115
162
  - Anything that opens a socket calls `markListening(server.url.origin)` and releases it on close.
116
163
  That is what tells the sealed test network a loopback request is this process, not egress.
117
164
 
165
+ ## Metrics: same seam as tracing, one signal over
166
+
167
+ ```ts
168
+ const published = counter('posts_published_total', { description: 'posts published' });
169
+ published.add(1, { plan: 'pro' });
170
+
171
+ gauge('queue_depth', { observe: () => pending() }); // read at scrape time, never stale
172
+ histogram('render_duration_seconds').record(ms / 1000);
173
+
174
+ metricsText(); // the /metrics body, at METRICS_PATH, METRICS_CONTENT_TYPE
175
+ collectMetrics(); // the same numbers as data, for a MetricExporter
176
+ ```
177
+
178
+ | | |
179
+ |---|---|
180
+ | Kinds | `counter` (monotonic sum), `gauge` (`record` / `add`, or an async `observe`), `histogram` (explicit bounds, OTel's default latency set) |
181
+ | Temporality | cumulative, as OTel defines it — a read never resets a counter, so two scrapers cannot steal each other's samples |
182
+ | Names | lowercase `snake_case`, the intersection every exposition format accepts. Dotted OTel names survive OTLP and die at a Prometheus scrape |
183
+ | Attributes | `string \| number \| boolean` only — each distinct set is a stored series, so a user id here is an outage |
184
+ | Driver seam | `MetricExporter`, defaulting to a no-op. `memoryMetricExporter()` for tests, `startMetricExport(ms)` for a periodic push |
185
+ | Not shipped | an OTLP client. It is bytes on a wire and a dependency; the seam is here, the driver is yours — or scrape `/metrics` with an agent that already speaks it |
186
+
187
+ `runtime-metrics.ts` holds the series every process emits, and `SCALING_METRICS` maps each
188
+ `ScalingSignal` from `roles.ts` to the one that carries it — so the role table, the chart and the
189
+ process cannot drift apart:
190
+
191
+ | Role scales on | Series | Instrument |
192
+ |---|---|---|
193
+ | `rps` | `http_requests_total` | counter; `rps` is a **rate** the adapter derives (`rate(http_requests_total[1m])`), never a stored number |
194
+ | `ws-connections` | `connections` | gauge, `+1`/`-1` |
195
+ | `queue-depth` | `queue_depth` | gauge, by `queue` label |
196
+
197
+ `As of 2026-08` all three are emitted and scraped. One call site per package — `recordRequest`
198
+ from `@ultimat3/http`'s pipeline, `recordConnection` from `@ultimat3/realtime`'s socket table,
199
+ `recordQueueDepth` from `@ultimat3/jobs`' worker loop — and `@ultimat3/cli` serves `metricsText()`
200
+ at `METRICS_PATH` on `METRICS_PORT` (9090), for every role rather than only the ones that open an
201
+ HTTP socket. Labels are route **patterns**, status **classes** and queue names: nothing
202
+ per-user, per-id or attacker-chosen ever becomes a series.
203
+
118
204
  ## One cursor, everywhere
119
205
 
120
206
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,123 @@
1
+ // Single responsibility: `.env.example` is a PROJECTION of the `defineEnv()` schema, never a
2
+ // second hand-maintained list. Render it from the declarations, and report drift when the file on
3
+ // disk has fallen behind. Loading `.env` itself is Bun's job — see `envFileCandidates()`.
4
+
5
+ import type { EnvSchema, EnvVarDecl } from './env';
6
+ import { type CodedErrorInit, UltimateError } from './errors';
7
+
8
+ export class EnvExampleDriftError extends UltimateError {
9
+ static readonly code = 'X_ENV_EXAMPLE_DRIFT';
10
+ override readonly name = 'EnvExampleDriftError';
11
+ constructor(init: CodedErrorInit) {
12
+ super({ ...init, code: EnvExampleDriftError.code });
13
+ }
14
+ }
15
+
16
+ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
17
+
18
+ export const ENV_EXAMPLE_PATH = '.env.example';
19
+
20
+ /**
21
+ * What Bun loads by itself, lowest precedence first — measured against Bun 1.3, not assumed.
22
+ *
23
+ * The mode is NOT `ULTIMATE_ENV` and not even `NODE_ENV` verbatim: Bun reads `.env.production`
24
+ * for `NODE_ENV=production`, `.env.test` for `test`, and `.env.development` for **everything
25
+ * else, `staging` included**. So a `.env.staging` file is never read, which is why a named
26
+ * environment is carried by real environment variables (`ULTIMATE_ENV` plus the platform's own
27
+ * config) and never by a per-environment dotenv file.
28
+ */
29
+ export function envFileCandidates(nodeEnv?: string | undefined): readonly string[] {
30
+ const mode = nodeEnv === 'production' || nodeEnv === 'test' ? nodeEnv : 'development';
31
+ const files = ['.env', `.env.${mode}`];
32
+ // Bun deliberately skips `.env.local` under test so a personal override cannot change a suite.
33
+ return mode === 'test' ? files : [...files, '.env.local'];
34
+ }
35
+
36
+ function typeLabel(decl: EnvVarDecl): string {
37
+ return decl.type === 'enum' ? decl.values.join(' | ') : decl.type;
38
+ }
39
+
40
+ function annotation(decl: EnvVarDecl): string {
41
+ const parts = [decl.required === false ? 'optional' : 'required', typeLabel(decl)];
42
+ if (decl.secret === true) parts.push('secret');
43
+ if (decl.role !== undefined) {
44
+ parts.push(`role ${(typeof decl.role === 'string' ? [decl.role] : decl.role).join('/')}`);
45
+ }
46
+ return parts.join(' · ');
47
+ }
48
+
49
+ /**
50
+ * A secret never carries a value here even when the declaration has a default: this file is
51
+ * committed, and a placeholder that happens to work is a credential nobody rotates.
52
+ */
53
+ function exampleValue(decl: EnvVarDecl): string {
54
+ if (decl.secret === true) return '';
55
+ if (decl.default !== undefined) return String(decl.default);
56
+ return decl.type === 'enum' ? (decl.values[0] ?? '') : '';
57
+ }
58
+
59
+ export interface EnvExampleOptions {
60
+ /** Extra keys the app sets outside the schema (`ROLE`, `ULTIMATE_ENV`), rendered commented. */
61
+ readonly extras?: readonly string[] | undefined;
62
+ }
63
+
64
+ /** Deterministic: declaration order in, declaration order out, so a rewrite diffs to nothing. */
65
+ export function renderEnvExample(schema: EnvSchema, options?: EnvExampleOptions): string {
66
+ const lines = [
67
+ '# Generated from defineEnv() — regenerate with renderEnvExample() from @ultimat3/core.',
68
+ '# Commit this file. Never commit .env: Bun loads .env, .env.<mode> and .env.local for you.',
69
+ ];
70
+ for (const [key, decl] of Object.entries(schema)) {
71
+ lines.push('');
72
+ if (decl.description !== undefined) lines.push(`# ${decl.description}`);
73
+ lines.push(`# ${annotation(decl)}`);
74
+ lines.push(`${key}=${exampleValue(decl)}`);
75
+ }
76
+ for (const extra of options?.extras ?? []) lines.push('', `# ${extra}=`);
77
+ return `${lines.join('\n')}\n`;
78
+ }
79
+
80
+ /** Keys only — values in a dotenv file are Bun's to parse, and half of them are placeholders. */
81
+ export function parseEnvKeys(text: string): readonly string[] {
82
+ const keys: string[] = [];
83
+ for (const raw of text.split('\n')) {
84
+ const line = raw.trim().replace(/^export\s+/, '');
85
+ if (line === '' || line.startsWith('#')) continue;
86
+ const separator = line.indexOf('=');
87
+ if (separator <= 0) continue;
88
+ const key = line.slice(0, separator).trim();
89
+ if (ENV_KEY_RE.test(key)) keys.push(key);
90
+ }
91
+ return keys;
92
+ }
93
+
94
+ export interface EnvExampleReport {
95
+ readonly ok: boolean;
96
+ /** Declared in the schema, absent from the file. Always a defect. */
97
+ readonly missing: readonly string[];
98
+ /** In the file, not in the schema. Reported, never fatal — apps set keys nothing declares. */
99
+ readonly extra: readonly string[];
100
+ }
101
+
102
+ export function checkEnvExample(schema: EnvSchema, text: string): EnvExampleReport {
103
+ const declared = Object.keys(schema);
104
+ const present = new Set(parseEnvKeys(text));
105
+ const missing = declared.filter((key) => !present.has(key));
106
+ const extra = [...present].filter((key) => !declared.includes(key));
107
+ return { ok: missing.length === 0, missing, extra };
108
+ }
109
+
110
+ /**
111
+ * Throws `X_ENV_EXAMPLE_DRIFT` when the committed example has fallen behind the schema — the
112
+ * failure an agent hits *before* a teammate hits `X_ENV_MISSING` on a variable nobody told them
113
+ * about.
114
+ */
115
+ export function assertEnvExample(schema: EnvSchema, text: string, path = ENV_EXAMPLE_PATH): void {
116
+ const report = checkEnvExample(schema, text);
117
+ if (report.ok) return;
118
+ throw new EnvExampleDriftError({
119
+ cause: `${path} does not declare ${report.missing.join(', ')}, declared by defineEnv()`,
120
+ fix: `Bun.write('${path}', renderEnvExample(schema)) — regenerate it from the declarations`,
121
+ meta: { path, missing: report.missing, extra: report.extra },
122
+ });
123
+ }
package/src/env.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // discover six missing variables.
4
4
 
5
5
  import { EnvMissingError } from './errors';
6
- import { redactKeys } from './logger';
6
+ import { REDACTED, redactKeys } from './logger';
7
7
  import { type Role, resolveRole } from './roles';
8
8
 
9
9
  export type EnvVarType = 'string' | 'url' | 'number' | 'integer' | 'port' | 'boolean' | 'enum';
@@ -231,6 +231,23 @@ export function defineEnv<const S extends EnvSchema>(schema: S, options?: EnvOpt
231
231
  return Object.freeze(report.values) as Env<S>;
232
232
  }
233
233
 
234
+ /**
235
+ * The resolved values with every `secret: true` key replaced. `checkEnv().values` carries the REAL
236
+ * values because `defineEnv()` has to return them — so anything that PRINTS a report (`x env check
237
+ * --json`, a doctor line, a log field) renders this instead, and the masking lives in one place
238
+ * rather than at each printer.
239
+ */
240
+ export function maskedEnvValues(
241
+ schema: EnvSchema,
242
+ values: Readonly<Record<string, unknown>>,
243
+ ): Readonly<Record<string, unknown>> {
244
+ const out: Record<string, unknown> = {};
245
+ for (const [key, value] of Object.entries(values)) {
246
+ out[key] = schema[key]?.secret === true && value !== undefined ? REDACTED : value;
247
+ }
248
+ return Object.freeze(out);
249
+ }
250
+
234
251
  export interface EnvVarSummary {
235
252
  readonly key: string;
236
253
  readonly type: EnvVarType;
@@ -0,0 +1,74 @@
1
+ // Single responsibility: which named environment this process is running as. ONE concept, one
2
+ // key (`ULTIMATE_ENV`), one spelling per value — the twin of `roles.ts`: `ROLE` says what the
3
+ // process does, `ULTIMATE_ENV` says which deploy it belongs to.
4
+
5
+ import { type CodedErrorInit, UltimateError } from './errors';
6
+
7
+ export class EnvironmentInvalidError extends UltimateError {
8
+ static readonly code = 'X_ENVIRONMENT_INVALID';
9
+ override readonly name = 'EnvironmentInvalidError';
10
+ constructor(init: CodedErrorInit) {
11
+ super({ ...init, code: EnvironmentInvalidError.code });
12
+ }
13
+ }
14
+
15
+ /**
16
+ * The spellings are `NODE_ENV`'s, plus `staging`. Inventing `dev`/`prod` aliases would mean two
17
+ * ways to write one environment, and `NODE_ENV=production` — which every container image and
18
+ * platform already sets — would have to be translated at every read.
19
+ */
20
+ export const ENVIRONMENTS = ['development', 'test', 'staging', 'production'] as const;
21
+
22
+ export type Environment = (typeof ENVIRONMENTS)[number];
23
+
24
+ export const DEFAULT_ENVIRONMENT: Environment = 'development';
25
+
26
+ /** The one key. `NODE_ENV` is read only as a fallback, because platforms set it for us. */
27
+ export const ENVIRONMENT_KEY = 'ULTIMATE_ENV';
28
+
29
+ export function isEnvironment(value: unknown): value is Environment {
30
+ return typeof value === 'string' && (ENVIRONMENTS as readonly string[]).includes(value);
31
+ }
32
+
33
+ export interface ResolveEnvironmentOptions {
34
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
35
+ readonly fallback?: Environment | undefined;
36
+ }
37
+
38
+ /**
39
+ * `ULTIMATE_ENV`, else `NODE_ENV`, else `development`.
40
+ *
41
+ * A set-but-unknown `ULTIMATE_ENV` throws: it is the framework's key, so a typo in it is always a
42
+ * mistake, exactly as `resolveRole()` treats `ROLE`. A set-but-unknown `NODE_ENV` does not throw —
43
+ * it is not ours to police, and CI images set it to values ("ci", "qa") that must not stop a boot.
44
+ */
45
+ export function resolveEnvironment(options?: ResolveEnvironmentOptions): Environment {
46
+ const source = options?.env ?? (process.env as Record<string, string | undefined>);
47
+ const declared = source[ENVIRONMENT_KEY];
48
+ if (declared !== undefined && declared !== '') {
49
+ if (isEnvironment(declared)) return declared;
50
+ throw new EnvironmentInvalidError({
51
+ cause: `${ENVIRONMENT_KEY}="${declared}" is not one of ${ENVIRONMENTS.join(' | ')}`,
52
+ fix: `export ${ENVIRONMENT_KEY}=${ENVIRONMENTS.join('|')} — one of those exact values`,
53
+ meta: { key: ENVIRONMENT_KEY, received: declared, allowed: ENVIRONMENTS },
54
+ });
55
+ }
56
+ const inherited = source['NODE_ENV'];
57
+ if (isEnvironment(inherited)) return inherited;
58
+ return options?.fallback ?? DEFAULT_ENVIRONMENT;
59
+ }
60
+
61
+ /** The one production test. Anything that is not literally `production` is not production. */
62
+ export function isProduction(options?: ResolveEnvironmentOptions): boolean {
63
+ return resolveEnvironment(options) === 'production';
64
+ }
65
+
66
+ /**
67
+ * True where a shipped development default (a signing key, a stub driver, an open CORS rule) is
68
+ * still acceptable. `staging` is deliberately NOT included: staging exists to fail the way
69
+ * production fails.
70
+ */
71
+ export function isLocal(options?: ResolveEnvironmentOptions): boolean {
72
+ const environment = resolveEnvironment(options);
73
+ return environment === 'development' || environment === 'test';
74
+ }
@@ -32,7 +32,9 @@ const CORE_CODE_TITLES = {
32
32
  X_CURSOR_INVALID: 'pagination cursor is malformed, tampered with or from another query',
33
33
  X_CURSOR_SECRET_DEV: 'cursors are signed with the shipped development key',
34
34
  X_DRAINING: 'process is draining and refuses new work',
35
+ X_ENV_EXAMPLE_DRIFT: '.env.example does not declare every variable the schema requires',
35
36
  X_ENV_MISSING: 'required environment variables are missing or invalid',
37
+ X_ENVIRONMENT_INVALID: 'ULTIMATE_ENV is not a known environment',
36
38
  X_ERROR_CODE_DUPLICATE: 'error code registered twice',
37
39
  X_ID_INVALID: 'value is not a valid id',
38
40
  X_IMAGE_DECODE_FAILED: 'image bytes are malformed, truncated or internally inconsistent',
@@ -40,6 +42,8 @@ const CORE_CODE_TITLES = {
40
42
  X_IMAGE_UNSUPPORTED: 'the built-in image pipeline cannot read or write this format',
41
43
  X_INTERNAL: 'unexpected internal framework error',
42
44
  X_INVARIANT: 'invariant violated',
45
+ X_METRIC_NAME_INVALID: 'metric name is malformed or already declared with another kind',
46
+ X_METRIC_VALUE_INVALID: 'metric value is not recordable',
43
47
  X_NO_CONTEXT: 'no request context is active',
44
48
  X_NOT_IMPLEMENTED: 'this driver does not implement the requested feature',
45
49
  X_REGISTRAR_CONFLICT: 'two different registrars are loaded for one primitive kind',
package/src/index.ts CHANGED
@@ -74,7 +74,28 @@ export type {
74
74
  EnvVarSummary,
75
75
  EnvVarType,
76
76
  } from './env';
77
- export { checkEnv, defineEnv, describeEnv } from './env';
77
+ export { checkEnv, defineEnv, describeEnv, maskedEnvValues } from './env';
78
+ export type { EnvExampleOptions, EnvExampleReport } from './env-example';
79
+ export {
80
+ assertEnvExample,
81
+ checkEnvExample,
82
+ ENV_EXAMPLE_PATH,
83
+ EnvExampleDriftError,
84
+ envFileCandidates,
85
+ parseEnvKeys,
86
+ renderEnvExample,
87
+ } from './env-example';
88
+ export type { Environment, ResolveEnvironmentOptions } from './environment';
89
+ export {
90
+ DEFAULT_ENVIRONMENT,
91
+ ENVIRONMENT_KEY,
92
+ ENVIRONMENTS,
93
+ EnvironmentInvalidError,
94
+ isEnvironment,
95
+ isLocal,
96
+ isProduction,
97
+ resolveEnvironment,
98
+ } from './environment';
78
99
  export type {
79
100
  CoreErrorCode,
80
101
  ErrorCodeDeclaration,
@@ -210,6 +231,41 @@ export {
210
231
  redactKeys,
211
232
  setLoggerContextFields,
212
233
  } from './logger';
234
+ export type {
235
+ Counter,
236
+ Gauge,
237
+ GaugeOptions,
238
+ Histogram,
239
+ HistogramOptions,
240
+ HistogramPoint,
241
+ InstrumentOptions,
242
+ MemoryMetricExporter,
243
+ MetricAttributes,
244
+ MetricAttributeValue,
245
+ MetricCollection,
246
+ MetricDescriptor,
247
+ MetricExporter,
248
+ MetricKind,
249
+ MetricPoint,
250
+ MetricsOptions,
251
+ ReadableMetric,
252
+ } from './metrics';
253
+ export {
254
+ collectMetrics,
255
+ configureMetrics,
256
+ counter,
257
+ DEFAULT_HISTOGRAM_BOUNDS,
258
+ exportMetrics,
259
+ gauge,
260
+ histogram,
261
+ MetricNameInvalidError,
262
+ MetricValueInvalidError,
263
+ memoryMetricExporter,
264
+ noopMetricExporter,
265
+ resetMetrics,
266
+ startMetricExport,
267
+ } from './metrics';
268
+ export { METRICS_CONTENT_TYPE, METRICS_PATH, metricsText } from './metrics-text';
213
269
  export type { ModuleRegistrar, PrimitiveKind, RegisteredPrimitive } from './registrar';
214
270
  export {
215
271
  hasPrimitiveRegistrar,
@@ -222,6 +278,27 @@ export type { Err, Ok, Result } from './result';
222
278
  export { err, isErr, isOk, map, mapErr, ok, tryCatch, unwrap, unwrapOr } from './result';
223
279
  export type { ResolveRoleOptions, Role, RoleInfo, ScalingSignal } from './roles';
224
280
  export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
281
+ export type { RequestSample } from './runtime-metrics';
282
+ export {
283
+ connections,
284
+ jobs,
285
+ queueDepth,
286
+ recordConnection,
287
+ recordJob,
288
+ recordQueueDepth,
289
+ recordRequest,
290
+ requestDuration,
291
+ requests,
292
+ SCALING_METRICS,
293
+ } from './runtime-metrics';
294
+ export type { Secret } from './secret';
295
+ export {
296
+ isSecret,
297
+ revealOptionalSecret,
298
+ revealSecret,
299
+ SECRET_BRAND,
300
+ secret,
301
+ } from './secret';
225
302
  export type { ServiceFactory } from './service';
226
303
  export { defineService, resetServices } from './service';
227
304
  export type {
@@ -248,6 +325,7 @@ export {
248
325
  noopExporter,
249
326
  parseTraceparent,
250
327
  resetTelemetry,
328
+ serviceResource,
251
329
  startSpan,
252
330
  traceparent,
253
331
  withSpan,
package/src/logger.ts CHANGED
@@ -3,6 +3,11 @@
3
3
 
4
4
  import { type Clock, systemClock } from './clock';
5
5
  import { isUltimateError } from './errors';
6
+ import { isSecret, REDACTED } from './secret';
7
+
8
+ // Re-exported, not redefined: `secret.ts` owns the placeholder because a `Secret` has to render
9
+ // it without importing the logger, and two constants spelled the same is one rename from a leak.
10
+ export { REDACTED } from './secret';
6
11
 
7
12
  export const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'] as const;
8
13
 
@@ -43,8 +48,6 @@ export interface LoggerOptions {
43
48
  readonly writer?: ((line: string, level: LogLevel) => void) | undefined;
44
49
  }
45
50
 
46
- export const REDACTED = '[redacted]';
47
-
48
51
  const redactedKeys = new Set<string>([
49
52
  'password',
50
53
  'token',
@@ -82,6 +85,9 @@ function defaultWriter(line: string, level: LogLevel): void {
82
85
 
83
86
  function serialiseValue(value: unknown, depth: number): unknown {
84
87
  if (value === null || typeof value !== 'object') return value;
88
+ // Before every other branch: a `Secret` is redacted by VALUE, so it stays redacted under a key
89
+ // nobody listed — `{ dsn: secret(url) }` is the leak key-name redaction cannot see.
90
+ if (isSecret(value)) return REDACTED;
85
91
  if (value instanceof Date) return value.toISOString();
86
92
  if (isUltimateError(value)) return value.toJSON();
87
93
  if (value instanceof Error) return { name: value.name, message: value.message };
@@ -0,0 +1,81 @@
1
+ // Single responsibility: render a `MetricCollection` as the Prometheus/OpenMetrics text format —
2
+ // the payload behind `/metrics`. A serialisation, not a vendor: it is what a Kubernetes metric
3
+ // adapter, a Grafana Agent, a Datadog agent and an OTel collector all already read (axiom 7).
4
+
5
+ import type { MetricAttributes, MetricPoint, ReadableMetric } from './metrics';
6
+ import { collectMetrics, type HistogramPoint, type MetricCollection } from './metrics';
7
+
8
+ export const METRICS_PATH = '/metrics';
9
+
10
+ export const METRICS_CONTENT_TYPE = 'text/plain; version=0.0.4; charset=utf-8';
11
+
12
+ /** The exposition format escapes exactly these three, and nothing else. */
13
+ function escapeLabel(value: string): string {
14
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n');
15
+ }
16
+
17
+ function labels(attributes: MetricAttributes, extra?: readonly [string, string]): string {
18
+ const pairs = Object.entries(attributes)
19
+ .sort(([a], [b]) => (a < b ? -1 : 1))
20
+ .map(([key, value]) => `${key}="${escapeLabel(String(value))}"`);
21
+ if (extra !== undefined) pairs.push(`${extra[0]}="${escapeLabel(extra[1])}"`);
22
+ return pairs.length === 0 ? '' : `{${pairs.join(',')}}`;
23
+ }
24
+
25
+ /** `+Inf` and `NaN` are spelled out; everything else is plain decimal. */
26
+ function number(value: number): string {
27
+ if (value === Number.POSITIVE_INFINITY) return '+Inf';
28
+ if (value === Number.NEGATIVE_INFINITY) return '-Inf';
29
+ return Number.isNaN(value) ? 'NaN' : String(value);
30
+ }
31
+
32
+ function isHistogramPoint(point: MetricPoint): point is HistogramPoint {
33
+ return 'buckets' in point;
34
+ }
35
+
36
+ /**
37
+ * A histogram renders as three families — `_bucket`, `_sum`, `_count` — with CUMULATIVE bucket
38
+ * counts. Storing them cumulatively instead would make every record a loop over the bounds.
39
+ */
40
+ function histogramLines(name: string, point: HistogramPoint): readonly string[] {
41
+ const lines: string[] = [];
42
+ let running = 0;
43
+ point.bounds.forEach((bound, index) => {
44
+ running += point.buckets[index] ?? 0;
45
+ lines.push(`${name}_bucket${labels(point.attributes, ['le', number(bound)])} ${running}`);
46
+ });
47
+ running += point.buckets[point.bounds.length] ?? 0;
48
+ lines.push(`${name}_bucket${labels(point.attributes, ['le', '+Inf'])} ${running}`);
49
+ lines.push(`${name}_sum${labels(point.attributes)} ${number(point.value)}`);
50
+ lines.push(`${name}_count${labels(point.attributes)} ${point.count}`);
51
+ return lines;
52
+ }
53
+
54
+ function metricLines(metric: ReadableMetric): readonly string[] {
55
+ const { name, kind, description, unit } = metric.descriptor;
56
+ const help = unit === '1' || unit === '' ? description : `${description} (${unit})`;
57
+ const lines = [`# HELP ${name} ${escapeLabel(help)}`, `# TYPE ${name} ${kind}`];
58
+ for (const point of metric.points) {
59
+ if (kind === 'histogram' && isHistogramPoint(point)) {
60
+ lines.push(...histogramLines(name, point));
61
+ continue;
62
+ }
63
+ lines.push(`${name}${labels(point.attributes)} ${number(point.value)}`);
64
+ }
65
+ return lines;
66
+ }
67
+
68
+ /**
69
+ * The scrape body. `target_info` carries the service identity as labels, which is how OTel's own
70
+ * Prometheus mapping does it — the alternative is stamping every series with the same two labels.
71
+ */
72
+ export function metricsText(collection: MetricCollection = collectMetrics()): string {
73
+ const { serviceName, serviceVersion } = collection.resource;
74
+ const lines = [
75
+ '# HELP target_info the service these metrics describe',
76
+ '# TYPE target_info gauge',
77
+ `target_info{service_name="${escapeLabel(serviceName)}",service_version="${escapeLabel(serviceVersion)}"} 1`,
78
+ ...collection.metrics.flatMap(metricLines),
79
+ ];
80
+ return `${lines.join('\n')}\n`;
81
+ }
package/src/metrics.ts ADDED
Binary file
@@ -0,0 +1,86 @@
1
+ // Single responsibility: the metrics EVERY Ultimate process emits, named to match what the
2
+ // deploy chart already scales on. One place decides these names — `roles.ts` says what a role
3
+ // scales on, this says which series carries it, and `docker/helm` reads the same three words.
4
+
5
+ import type { Counter, Gauge } from './metrics';
6
+ import { counter, gauge, type Histogram, histogram } from './metrics';
7
+ import type { ScalingSignal } from './roles';
8
+
9
+ /**
10
+ * The instrument behind each role's scaling signal.
11
+ *
12
+ * `rps` is a RATE, and a rate is derived, never stored: the process emits a monotonic counter and
13
+ * the adapter differentiates it (`rate(http_requests_total[1m])`), which is also what the chart's
14
+ * own comment ("via the ingress metric adapter") already assumes. The other two are instantaneous
15
+ * values a scrape can read directly, so their series names are the chart's words verbatim.
16
+ */
17
+ export const SCALING_METRICS: Readonly<Record<ScalingSignal, string | null>> = Object.freeze({
18
+ rps: 'http_requests_total',
19
+ 'ws-connections': 'connections',
20
+ 'queue-depth': 'queue_depth',
21
+ singleton: null,
22
+ 'run-once': null,
23
+ 'per-database': null,
24
+ });
25
+
26
+ export const requests: Counter = counter('http_requests_total', {
27
+ unit: '{request}',
28
+ description: 'HTTP requests served, by route, method and status class',
29
+ });
30
+
31
+ export const requestDuration: Histogram = histogram('http_request_duration_seconds', {
32
+ unit: 's',
33
+ description: 'HTTP server request duration',
34
+ });
35
+
36
+ export const connections: Gauge = gauge('connections', {
37
+ unit: '{connection}',
38
+ description: 'Live websocket connections held by this process',
39
+ });
40
+
41
+ export const queueDepth: Gauge = gauge('queue_depth', {
42
+ unit: '{job}',
43
+ description: 'Jobs waiting to be picked up, by queue',
44
+ });
45
+
46
+ export const jobs: Counter = counter('jobs_total', {
47
+ unit: '{job}',
48
+ description: 'Background jobs finished, by queue and outcome',
49
+ });
50
+
51
+ export interface RequestSample {
52
+ readonly method: string;
53
+ /** The route PATTERN (`/posts/:id`), never the concrete path — one series per pattern. */
54
+ readonly route: string;
55
+ readonly status: number;
56
+ readonly durationMs: number;
57
+ }
58
+
59
+ /**
60
+ * One call per served request, from the HTTP pipeline. `status` becomes a status CLASS (`2xx`)
61
+ * because a label per status code multiplies every series by the number of codes an app can
62
+ * return, and no autoscaler asks the difference between 201 and 204.
63
+ */
64
+ export function recordRequest(sample: RequestSample): void {
65
+ const attributes = {
66
+ method: sample.method,
67
+ route: sample.route,
68
+ status: `${Math.floor(sample.status / 100)}xx`,
69
+ };
70
+ requests.add(1, attributes);
71
+ requestDuration.record(sample.durationMs / 1000, attributes);
72
+ }
73
+
74
+ /** `+1` on connect, `-1` on close, from the realtime transport. */
75
+ export function recordConnection(delta: number): void {
76
+ connections.add(delta);
77
+ }
78
+
79
+ /** Absolute depth, from the worker's own queue read. */
80
+ export function recordQueueDepth(queue: string, depth: number): void {
81
+ queueDepth.record(depth, { queue });
82
+ }
83
+
84
+ export function recordJob(queue: string, outcome: 'ok' | 'failed' | 'dead'): void {
85
+ jobs.add(1, { queue, outcome });
86
+ }
package/src/secret.ts ADDED
@@ -0,0 +1,67 @@
1
+ // Single responsibility: a value that cannot be printed by accident. Key-name redaction only
2
+ // catches a secret travelling under a name someone remembered to list; a `Secret` box redacts by
3
+ // VALUE, so the same string is safe in a log line, an error `meta`, a manifest and a snapshot.
4
+
5
+ export const REDACTED = '[redacted]';
6
+
7
+ /**
8
+ * Structural brand, not `instanceof`: two copies of `@ultimat3/core` in one dependency tree must
9
+ * still recognise each other's secrets, or redaction silently stops applying at the seam.
10
+ */
11
+ export const SECRET_BRAND: unique symbol = Symbol.for('ultimate.secret');
12
+
13
+ /** Bun and Node both honour this when rendering a value in `console.log`. */
14
+ const INSPECT = Symbol.for('nodejs.util.inspect.custom');
15
+
16
+ export interface Secret {
17
+ readonly [SECRET_BRAND]: true;
18
+ /** Non-secret name for the value — the env key, the config field. Safe to print. */
19
+ readonly label: string;
20
+ toString(): string;
21
+ toJSON(): string;
22
+ }
23
+
24
+ interface SecretInternal extends Secret {
25
+ reveal(): string;
26
+ }
27
+
28
+ const hidden = (value: unknown): PropertyDescriptor => ({
29
+ value,
30
+ enumerable: false,
31
+ writable: false,
32
+ configurable: false,
33
+ });
34
+
35
+ /**
36
+ * Box a value so every serialisation path renders `[redacted]`: `String()`, template literals,
37
+ * `+`, `JSON.stringify`, `console.log`, and the logger. `revealSecret()` is the only way out, and
38
+ * it is a free function on purpose — one greppable call site per place a secret is actually used.
39
+ *
40
+ * Everything except `label` is non-enumerable, so `{ ...token }`, `Object.entries(token)` and a
41
+ * structured-clone of it cannot carry the value back out of the box.
42
+ */
43
+ export function secret(value: string, label = 'secret'): Secret {
44
+ const boxed = Object.defineProperties({ label } as { label: string }, {
45
+ [SECRET_BRAND]: hidden(true),
46
+ reveal: hidden(() => value),
47
+ toString: hidden(() => REDACTED),
48
+ toJSON: hidden(() => REDACTED),
49
+ [INSPECT]: hidden(() => REDACTED),
50
+ [Symbol.toPrimitive]: hidden(() => REDACTED),
51
+ }) as unknown as SecretInternal;
52
+ return Object.freeze(boxed);
53
+ }
54
+
55
+ export function isSecret(value: unknown): value is Secret {
56
+ return typeof value === 'object' && value !== null && SECRET_BRAND in value;
57
+ }
58
+
59
+ /** The one documented way to read a secret. Every call site is a place worth reviewing. */
60
+ export function revealSecret(value: Secret): string {
61
+ return (value as SecretInternal).reveal();
62
+ }
63
+
64
+ /** Reveal only when there is something to reveal — for optional configuration. */
65
+ export function revealOptionalSecret(value: Secret | undefined): string | undefined {
66
+ return value === undefined ? undefined : revealSecret(value);
67
+ }
package/src/telemetry.ts CHANGED
@@ -139,6 +139,14 @@ export function resetTelemetry(): void {
139
139
  enabled = true;
140
140
  }
141
141
 
142
+ /**
143
+ * The service identity every signal carries. One resource for spans and metrics alike, as OTel
144
+ * defines it — a metric that named a different service than its own traces is unjoinable.
145
+ */
146
+ export function serviceResource(): SpanResource {
147
+ return resource;
148
+ }
149
+
142
150
  export function currentSpan(): Span | undefined {
143
151
  return activeSpan.getStore();
144
152
  }