@tmlmobilidade/go-utils-telemetry 20260922.1130.21

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.
@@ -0,0 +1,2 @@
1
+ export * from './logger/index.js';
2
+ export * from './timer/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './logger/index.js';
2
+ export * from './timer/index.js';
@@ -0,0 +1,8 @@
1
+ import { type Logger, type LogRenderer } from './types/logger.js';
2
+ /**
3
+ * Builds the public logger around one renderer.
4
+ *
5
+ * Every level method goes through the same `log` path, so their signatures cannot drift.
6
+ * Presentation helpers are plain console output and behave the same in every environment.
7
+ */
8
+ export declare function createLogger(render: LogRenderer): Logger;
@@ -0,0 +1,30 @@
1
+ import { divider } from './presentation/divider.js';
2
+ import { init } from './presentation/init.js';
3
+ import { spacer } from './presentation/spacer.js';
4
+ import { terminate } from './presentation/terminate.js';
5
+ import { title } from './presentation/title.js';
6
+ import { toRecord } from './to-record.js';
7
+ /**
8
+ * Builds the public logger around one renderer.
9
+ *
10
+ * Every level method goes through the same `log` path, so their signatures cannot drift.
11
+ * Presentation helpers are plain console output and behave the same in every environment.
12
+ */
13
+ export function createLogger(render) {
14
+ const log = (level, input) => render(toRecord(level, input));
15
+ return {
16
+ critical: input => log('critical', input),
17
+ debug: input => log('debug', input),
18
+ divider,
19
+ error: input => log('error', input),
20
+ info: input => log('info', input),
21
+ init,
22
+ log,
23
+ progress: input => log('progress', input),
24
+ spacer,
25
+ success: input => log('success', input),
26
+ terminate,
27
+ title,
28
+ warning: input => log('warning', input),
29
+ };
30
+ }
@@ -0,0 +1,10 @@
1
+ import { type Logger as LoggerApi } from './types/logger.js';
2
+ /**
3
+ * Shared application logger.
4
+ * Human-readable console output in development, OpenTelemetry JSON lines otherwise.
5
+ */
6
+ export declare const Logger: LoggerApi;
7
+ export type { LogLevel } from './levels.js';
8
+ export type { OtelLogRecord } from './render/structured.js';
9
+ export type { LogEntry, LogInput, LogRecord } from './types/entry.js';
10
+ export type { Logger as LoggerApi, LogRenderer } from './types/logger.js';
@@ -0,0 +1,12 @@
1
+ /* * */
2
+ import { createLogger } from './create-logger.js';
3
+ import { renderDev } from './render/dev.js';
4
+ import { renderStructured } from './render/structured.js';
5
+ /* * */
6
+ const IS_DEV = process.env.ENVIRONMENT === 'dev';
7
+ /* * */
8
+ /**
9
+ * Shared application logger.
10
+ * Human-readable console output in development, OpenTelemetry JSON lines otherwise.
11
+ */
12
+ export const Logger = createLogger(IS_DEV ? renderDev : renderStructured);
@@ -0,0 +1,38 @@
1
+ /** Semantic kind of a log line. Each level maps to exactly one OpenTelemetry severity. */
2
+ export type LogLevel = 'critical' | 'debug' | 'error' | 'info' | 'progress' | 'success' | 'warning';
3
+ /**
4
+ * OpenTelemetry severity for each level.
5
+ *
6
+ * `success` and `progress` are INFO: they exist so the development renderer can
7
+ * draw them differently, not because they carry a different severity.
8
+ */
9
+ export declare const SEVERITY: {
10
+ readonly critical: {
11
+ readonly number: 21;
12
+ readonly text: "FATAL";
13
+ };
14
+ readonly debug: {
15
+ readonly number: 5;
16
+ readonly text: "DEBUG";
17
+ };
18
+ readonly error: {
19
+ readonly number: 17;
20
+ readonly text: "ERROR";
21
+ };
22
+ readonly info: {
23
+ readonly number: 9;
24
+ readonly text: "INFO";
25
+ };
26
+ readonly progress: {
27
+ readonly number: 9;
28
+ readonly text: "INFO";
29
+ };
30
+ readonly success: {
31
+ readonly number: 9;
32
+ readonly text: "INFO";
33
+ };
34
+ readonly warning: {
35
+ readonly number: 13;
36
+ readonly text: "WARN";
37
+ };
38
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * OpenTelemetry severity for each level.
3
+ *
4
+ * `success` and `progress` are INFO: they exist so the development renderer can
5
+ * draw them differently, not because they carry a different severity.
6
+ */
7
+ export const SEVERITY = {
8
+ critical: { number: 21, text: 'FATAL' },
9
+ debug: { number: 5, text: 'DEBUG' },
10
+ error: { number: 17, text: 'ERROR' },
11
+ info: { number: 9, text: 'INFO' },
12
+ progress: { number: 9, text: 'INFO' },
13
+ success: { number: 9, text: 'INFO' },
14
+ warning: { number: 13, text: 'WARN' },
15
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Logs a divider line to visually separate sections in the console output.
3
+ *
4
+ * If a message is provided, it will be placed after an initial hyphen and padded with additional hyphens up to `size`.
5
+ * For example: divider("Section", 20) will print something like:
6
+ *
7
+ *
8
+ * - Section -----------
9
+ *
10
+ * @param message Optional message to display within the divider.
11
+ * @param size Total length of the divider line. Defaults to 75.
12
+ */
13
+ export declare function divider(message?: string, size?: number): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Logs a divider line to visually separate sections in the console output.
3
+ *
4
+ * If a message is provided, it will be placed after an initial hyphen and padded with additional hyphens up to `size`.
5
+ * For example: divider("Section", 20) will print something like:
6
+ *
7
+ *
8
+ * - Section -----------
9
+ *
10
+ * @param message Optional message to display within the divider.
11
+ * @param size Total length of the divider line. Defaults to 75.
12
+ */
13
+ export function divider(message, size = 75) {
14
+ console.log();
15
+ if (message) {
16
+ // Calculate the available space for hyphens after the message and account for hyphens and spaces
17
+ const hyphenCount = size - 2 - message.length;
18
+ console.log(`- ${message} ${'-'.repeat(hyphenCount < 1 ? 1 : hyphenCount)}`);
19
+ }
20
+ else {
21
+ console.log('-'.repeat(size));
22
+ }
23
+ console.log();
24
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Logs an initialization block with the current ISO timestamp.
3
+ *
4
+ * Output format:
5
+ * (blank line)
6
+ * --------------------... (matches timestamp width)
7
+ * 2023-01-01T12:34:56.789Z
8
+ * --------------------... (matches timestamp width)
9
+ * (blank line)
10
+ */
11
+ export declare function init(): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Logs an initialization block with the current ISO timestamp.
3
+ *
4
+ * Output format:
5
+ * (blank line)
6
+ * --------------------... (matches timestamp width)
7
+ * 2023-01-01T12:34:56.789Z
8
+ * --------------------... (matches timestamp width)
9
+ * (blank line)
10
+ */
11
+ export function init() {
12
+ const timestamp = new Date().toISOString();
13
+ console.log();
14
+ console.log('-'.repeat(timestamp.length));
15
+ console.log(timestamp);
16
+ console.log('-'.repeat(timestamp.length));
17
+ console.log();
18
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Prints the specified number of blank lines to stdout.
3
+ *
4
+ * This helper inserts padding in logs for readability, by emitting `lines`
5
+ * empty lines (default 1) using console.log().
6
+ *
7
+ * @param lines - Number of blank lines to print (default: 1)
8
+ *
9
+ * Example:
10
+ * spacer(); // prints 1 blank line
11
+ * spacer(3); // prints 3 blank lines
12
+ */
13
+ export declare function spacer(lines?: number): void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Prints the specified number of blank lines to stdout.
3
+ *
4
+ * This helper inserts padding in logs for readability, by emitting `lines`
5
+ * empty lines (default 1) using console.log().
6
+ *
7
+ * @param lines - Number of blank lines to print (default: 1)
8
+ *
9
+ * Example:
10
+ * spacer(); // prints 1 blank line
11
+ * spacer(3); // prints 3 blank lines
12
+ */
13
+ export function spacer(lines = 1) {
14
+ for (let i = 0; i < lines; i++) {
15
+ console.log();
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Logs a termination/done message surrounded by separator lines for emphasis.
3
+ *
4
+ * @param message - The message to highlight as terminated/completed.
5
+ */
6
+ export declare function terminate(message: string): void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Logs a termination/done message surrounded by separator lines for emphasis.
3
+ *
4
+ * @param message - The message to highlight as terminated/completed.
5
+ */
6
+ export function terminate(message) {
7
+ console.log();
8
+ console.log('-'.repeat(message.length));
9
+ console.log(message);
10
+ console.log('-'.repeat(message.length));
11
+ console.log();
12
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Logs a visually prominent title message to stdout, wrapped with spacing.
3
+ *
4
+ * @param message - The title or label to display.
5
+ */
6
+ export declare function title(message: string): void;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Logs a visually prominent title message to stdout, wrapped with spacing.
3
+ *
4
+ * @param message - The title or label to display.
5
+ */
6
+ export function title(message) {
7
+ console.log();
8
+ console.log(`▶︎ ${message}`);
9
+ console.log();
10
+ }
@@ -0,0 +1,8 @@
1
+ import { type LogRenderer } from '../types/logger.js';
2
+ /**
3
+ * Human-readable console line for local development.
4
+ *
5
+ * Prints optional spacing, `glyph message`, and the error stack when an error is attached.
6
+ * Attributes are not rendered.
7
+ */
8
+ export declare const renderDev: LogRenderer;
@@ -0,0 +1,30 @@
1
+ import { spacer } from '../presentation/spacer.js';
2
+ /**
3
+ * Glyph and console method per level. `satisfies` makes a missing level a compile error.
4
+ * The method is stored by name and resolved on each call so patched consoles are honoured.
5
+ */
6
+ const STYLE = {
7
+ critical: { glyph: '‼', method: 'error' },
8
+ debug: { glyph: '·', method: 'debug' },
9
+ error: { glyph: '✘', method: 'error' },
10
+ info: { glyph: '→', method: 'log' },
11
+ progress: { glyph: '•', method: 'log' },
12
+ success: { glyph: '✓', method: 'log' },
13
+ warning: { glyph: '⚠', method: 'warn' },
14
+ };
15
+ /**
16
+ * Human-readable console line for local development.
17
+ *
18
+ * Prints optional spacing, `glyph message`, and the error stack when an error is attached.
19
+ * Attributes are not rendered.
20
+ */
21
+ export const renderDev = (record) => {
22
+ const { glyph, method } = STYLE[record.level];
23
+ if (record.spacesBefore)
24
+ spacer(record.spacesBefore);
25
+ console[method](`${glyph} ${record.message}`);
26
+ if (record.error?.stack)
27
+ console[method](record.error.stack);
28
+ if (record.spacesAfter)
29
+ spacer(record.spacesAfter);
30
+ };
@@ -0,0 +1,21 @@
1
+ import { type LogRenderer } from '../types/logger.js';
2
+ /** OpenTelemetry-compatible JSON log record emitted in production. */
3
+ export interface OtelLogRecord {
4
+ /** Structured event attributes, including `exception.*` when an error is attached. */
5
+ attributes?: Record<string, unknown>;
6
+ /** Human-readable event message. */
7
+ body: string;
8
+ /** OpenTelemetry severity number. */
9
+ severity_number: number;
10
+ /** OpenTelemetry severity name. */
11
+ severity_text: string;
12
+ /** ISO 8601 event timestamp. */
13
+ timestamp: string;
14
+ }
15
+ /**
16
+ * One JSON line per record, for Grafana Alloy/Loki.
17
+ *
18
+ * Records at `error` severity and above go to stderr, everything else to stdout.
19
+ * `spacesBefore`/`spacesAfter` are development hints and are ignored here.
20
+ */
21
+ export declare const renderStructured: LogRenderer;
@@ -0,0 +1,29 @@
1
+ import { SEVERITY } from '../levels.js';
2
+ /** OpenTelemetry semantic-convention attributes for an attached error. */
3
+ function exceptionAttributes(error) {
4
+ if (!error)
5
+ return {};
6
+ return {
7
+ 'exception.message': error.message,
8
+ 'exception.stacktrace': error.stack,
9
+ 'exception.type': error.constructor.name,
10
+ };
11
+ }
12
+ /**
13
+ * One JSON line per record, for Grafana Alloy/Loki.
14
+ *
15
+ * Records at `error` severity and above go to stderr, everything else to stdout.
16
+ * `spacesBefore`/`spacesAfter` are development hints and are ignored here.
17
+ */
18
+ export const renderStructured = (record) => {
19
+ const { number, text } = SEVERITY[record.level];
20
+ const attributes = { ...record.attributes, ...exceptionAttributes(record.error) };
21
+ const line = {
22
+ attributes: Object.keys(attributes).length ? attributes : undefined,
23
+ body: record.message,
24
+ severity_number: number,
25
+ severity_text: text,
26
+ timestamp: new Date().toISOString(),
27
+ };
28
+ (number >= SEVERITY.error.number ? console.error : console.log)(JSON.stringify(line));
29
+ };
@@ -0,0 +1,9 @@
1
+ import { type LogLevel } from './levels.js';
2
+ import { type LogInput, type LogRecord } from './types/entry.js';
3
+ /**
4
+ * Normalises any accepted input into a record.
5
+ *
6
+ * `Error` is checked before the object form: an Error also satisfies the entry shape
7
+ * structurally, and treating it as one would drop its stack.
8
+ */
9
+ export declare function toRecord(level: LogLevel, input: LogInput): LogRecord;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Normalises any accepted input into a record.
3
+ *
4
+ * `Error` is checked before the object form: an Error also satisfies the entry shape
5
+ * structurally, and treating it as one would drop its stack.
6
+ */
7
+ export function toRecord(level, input) {
8
+ if (typeof input === 'string')
9
+ return { level, message: input };
10
+ if (input instanceof Error)
11
+ return { error: input, level, message: input.message };
12
+ return { ...input, level, message: input.message ?? input.error?.message ?? '' };
13
+ }
@@ -0,0 +1,33 @@
1
+ import { type LogLevel } from '../levels.js';
2
+ interface LogEntryFields {
3
+ /** Structured fields. Emitted as OpenTelemetry `attributes` in production; not rendered in development. */
4
+ attributes?: Record<string, unknown>;
5
+ /** Error to attach. Development prints its stack; production emits `exception.*` attributes. */
6
+ error?: Error;
7
+ /** Human-readable text. Defaults to `error.message` when omitted. */
8
+ message?: string;
9
+ /** Development only: blank lines printed after the entry. Ignored in production. */
10
+ spacesAfter?: number;
11
+ /** Development only: blank lines printed before the entry. Ignored in production. */
12
+ spacesBefore?: number;
13
+ }
14
+ /** Object form accepted by every level method. Requires a message or an error. */
15
+ export type LogEntry = LogEntryFields & ({
16
+ error: Error;
17
+ } | {
18
+ message: string;
19
+ });
20
+ /**
21
+ * Anything a level method accepts.
22
+ *
23
+ * `Error` is listed explicitly on purpose: an Error is structurally a `{ message: string }`,
24
+ * so without this member (and the `instanceof` check in `toRecord`) a bare Error would be
25
+ * treated as an entry with no error attached and its stack would be lost.
26
+ */
27
+ export type LogInput = Error | LogEntry | string;
28
+ /** Fully resolved entry handed to a renderer. */
29
+ export interface LogRecord extends Omit<LogEntryFields, 'message'> {
30
+ level: LogLevel;
31
+ message: string;
32
+ }
33
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { type LogLevel } from '../levels.js';
2
+ import { type LogInput, type LogRecord } from './entry.js';
3
+ /** Renders one resolved record. This is the only thing that differs between development and production. */
4
+ export type LogRenderer = (record: LogRecord) => void;
5
+ type LevelMethod = (input: LogInput) => void;
6
+ /**
7
+ * Public logger API. Identical in every environment; only rendering differs.
8
+ *
9
+ * Extending `Record<LogLevel, LevelMethod>` means adding a value to `LogLevel`
10
+ * without a matching method is a compile error.
11
+ */
12
+ export interface Logger extends Record<LogLevel, LevelMethod> {
13
+ divider(message?: string, size?: number): void;
14
+ init(): void;
15
+ /** Generic entry point. Every level method is shorthand for `log(level, input)`. */
16
+ log(level: LogLevel, input: LogInput): void;
17
+ spacer(lines?: number): void;
18
+ terminate(message: string): void;
19
+ title(message: string): void;
20
+ }
21
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Timer class for tracking elapsed time.
3
+ * Provides methods to get formatted time strings and raw time values.
4
+ */
5
+ export declare class Timer {
6
+ private startTime;
7
+ constructor();
8
+ /**
9
+ * Gets the elapsed time as a formatted string.
10
+ * @param reset - If true, resets the timer after getting the elapsed time
11
+ * @returns Formatted string (e.g., "1h 2m 3s 456ms" or "0ms")
12
+ */
13
+ get(reset?: boolean): string;
14
+ /**
15
+ * Gets the elapsed time in milliseconds.
16
+ * @param reset - If true, resets the timer after getting the elapsed time
17
+ * @returns Elapsed time in milliseconds
18
+ */
19
+ getMs(reset?: boolean): number;
20
+ /**
21
+ * Gets the elapsed time in seconds.
22
+ * @param reset - If true, resets the timer after getting the elapsed time
23
+ * @returns Elapsed time in seconds (with decimal precision)
24
+ */
25
+ getSeconds(reset?: boolean): number;
26
+ /**
27
+ * Resets the timer to the current time.
28
+ */
29
+ reset(): void;
30
+ /**
31
+ * Formats elapsed time in milliseconds to a human-readable string.
32
+ * @param elapsedTime - Time in milliseconds
33
+ * @returns Formatted string (e.g., "1h 2m 3s 456ms" or "0ms")
34
+ */
35
+ private formatElapsedTime;
36
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Timer class for tracking elapsed time.
3
+ * Provides methods to get formatted time strings and raw time values.
4
+ */
5
+ export class Timer {
6
+ startTime;
7
+ constructor() {
8
+ this.startTime = Date.now();
9
+ }
10
+ /**
11
+ * Gets the elapsed time as a formatted string.
12
+ * @param reset - If true, resets the timer after getting the elapsed time
13
+ * @returns Formatted string (e.g., "1h 2m 3s 456ms" or "0ms")
14
+ */
15
+ get(reset = false) {
16
+ const elapsedTime = Date.now() - this.startTime;
17
+ const result = this.formatElapsedTime(elapsedTime);
18
+ if (reset) {
19
+ this.reset();
20
+ }
21
+ return result;
22
+ }
23
+ /**
24
+ * Gets the elapsed time in milliseconds.
25
+ * @param reset - If true, resets the timer after getting the elapsed time
26
+ * @returns Elapsed time in milliseconds
27
+ */
28
+ getMs(reset = false) {
29
+ const elapsedTime = Date.now() - this.startTime;
30
+ if (reset) {
31
+ this.reset();
32
+ }
33
+ return elapsedTime;
34
+ }
35
+ /**
36
+ * Gets the elapsed time in seconds.
37
+ * @param reset - If true, resets the timer after getting the elapsed time
38
+ * @returns Elapsed time in seconds (with decimal precision)
39
+ */
40
+ getSeconds(reset = false) {
41
+ const elapsedTime = Date.now() - this.startTime;
42
+ const seconds = elapsedTime / 1000;
43
+ if (reset) {
44
+ this.reset();
45
+ }
46
+ return seconds;
47
+ }
48
+ /**
49
+ * Resets the timer to the current time.
50
+ */
51
+ reset() {
52
+ this.startTime = Date.now();
53
+ }
54
+ /**
55
+ * Formats elapsed time in milliseconds to a human-readable string.
56
+ * @param elapsedTime - Time in milliseconds
57
+ * @returns Formatted string (e.g., "1h 2m 3s 456ms" or "0ms")
58
+ */
59
+ formatElapsedTime(elapsedTime) {
60
+ // Ensure non-negative time
61
+ const time = Math.max(0, elapsedTime);
62
+ const hours = Math.floor(time / (1000 * 60 * 60));
63
+ const minutes = Math.floor(time / (1000 * 60)) % 60;
64
+ const seconds = Math.floor(time / 1000) % 60;
65
+ const milliseconds = time % 1000;
66
+ const parts = [];
67
+ if (hours > 0)
68
+ parts.push(`${hours}h`);
69
+ if (minutes > 0)
70
+ parts.push(`${minutes}m`);
71
+ if (seconds > 0)
72
+ parts.push(`${seconds}s`);
73
+ if (milliseconds > 0 || parts.length === 0) {
74
+ parts.push(`${milliseconds}ms`);
75
+ }
76
+ return parts.join(' ');
77
+ }
78
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@tmlmobilidade/go-utils-telemetry",
3
+ "version": "20260922.1130.21",
4
+ "author": {
5
+ "email": "iso@tmlmobilidade.pt",
6
+ "name": "TML-ISO"
7
+ },
8
+ "license": "AGPL-3.0-or-later",
9
+ "homepage": "https://go.tmlmobilidade.pt",
10
+ "bugs": {
11
+ "url": "https://github.com/tmlmobilidade/go/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/tmlmobilidade/go.git"
16
+ },
17
+ "keywords": [
18
+ "public transit",
19
+ "tml",
20
+ "transportes metropolitanos de lisboa",
21
+ "go"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "scripts": {
33
+ "build": "tsc && resolve-tspaths",
34
+ "lint": "eslint ./src/ && tsc --noEmit",
35
+ "lint:fix": "eslint ./src/ --fix",
36
+ "test": "tsx --test tests/*.test.ts",
37
+ "watch": "tsc-watch --onSuccess 'resolve-tspaths'"
38
+ },
39
+ "devDependencies": {
40
+ "@tmlmobilidade/go-utils-tsconfig": "*",
41
+ "@types/node": "26.4.0",
42
+ "resolve-tspaths": "0.8.23",
43
+ "tsc-watch": "7.2.1",
44
+ "tsx": "4.23.12",
45
+ "typescript": "6.0.3"
46
+ }
47
+ }