@ultimat3/core 10.0.0 → 11.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "10.0.0",
3
+ "version": "11.0.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/logger.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Single responsibility: structured JSON logging. One line per event, machine-readable by
2
2
  // default because the primary reader is an agent tailing `x logs --json`.
3
3
 
4
+ import { assert } from './assert';
4
5
  import { type Clock, systemClock } from './clock';
5
6
  import { renderCauseValue } from './error-render';
6
7
  import { isUltimateError } from './errors';
@@ -244,8 +245,29 @@ function envLevel(): LogLevel {
244
245
  : 'info';
245
246
  }
246
247
 
248
+ /**
249
+ * The level this logger enforces, refused when it is not one.
250
+ *
251
+ * `LEVEL_WEIGHT[level]` on anything else is `undefined`, and every `weight < undefined` is false —
252
+ * so the threshold failed OPEN and the logger emitted every line at every level. That is a `trace`
253
+ * stream out of a production process from one typo, which is the direction this read must never
254
+ * fail in. `LOG_LEVELS`, the same list `envLevel()` filters `LOG_LEVEL` through, because a level
255
+ * is typed here and arrives untyped: an `app.config.ts` value, a JSON file, a CLI flag.
256
+ */
257
+ function resolveLevel(declared: LogLevel): LogLevel {
258
+ assert(
259
+ (LOG_LEVELS as readonly unknown[]).includes(declared),
260
+ // `renderCauseValue`, never `JSON.stringify`: it raises on a bigint and on a cycle, and a
261
+ // level that arrived from a config file can be either — the refusal must not be replaced by
262
+ // a `TypeError` from building its own message.
263
+ `${renderCauseValue(declared)} is not a log level`,
264
+ `pass one of ${LOG_LEVELS.join(', ')} to createLogger({ level })`,
265
+ );
266
+ return declared;
267
+ }
268
+
247
269
  export function createLogger(options?: LoggerOptions): Logger {
248
- const level = options?.level ?? envLevel();
270
+ const level = options?.level === undefined ? envLevel() : resolveLevel(options.level);
249
271
  const bound = options?.fields ?? {};
250
272
  const clock = options?.clock ?? systemClock;
251
273
  const writer = options?.writer ?? defaultWriter;
package/src/metrics.ts CHANGED
@@ -174,14 +174,24 @@ export function resetMetrics(): void {
174
174
  }
175
175
  }
176
176
 
177
- /** Stable series key: attribute order must not create a second series for one label set. */
177
+ /**
178
+ * Stable series key: attribute order must not create a second series for one label set, and no
179
+ * label set may spell another one's key.
180
+ *
181
+ * `JSON.stringify` over the sorted pairs, because a DELIMITER cannot carry the second property:
182
+ * the key was the pairs joined by control characters (U+0000 inside a pair, U+0001 between them),
183
+ * and a value holding those bytes IS another set's key — `{ a: 'b\u0001c\u0000d' }` was
184
+ * `{ a: 'b', c: 'd' }`, so the point landed on whichever series arrived first and was exported
185
+ * under labels the caller never passed. Attribute values are app data. Quoting is the only total
186
+ * answer and is not slower: 644 ns/op against the join's 709, on a 3-label set. `String(value)`
187
+ * stays, so `1` and `'1'` are still one series rather than two rows an exporter renders alike.
188
+ */
178
189
  function seriesKey(attributes: MetricAttributes): string {
179
190
  const entries = Object.entries(attributes);
180
191
  if (entries.length === 0) return '';
181
- return entries
182
- .sort(([a], [b]) => (a < b ? -1 : 1))
183
- .map(([key, value]) => `${key}\u0000${String(value)}`)
184
- .join('');
192
+ return JSON.stringify(
193
+ entries.sort(([a], [b]) => (a < b ? -1 : 1)).map(([key, value]) => [key, String(value)]),
194
+ );
185
195
  }
186
196
 
187
197
  function finite(name: string, value: number): number {