All files / core/semantic-log/src logCall.ts

74.71% Statements 65/87
100% Branches 22/22
75% Functions 3/4
74.71% Lines 65/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 881x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 991x 991x 991x 393x 393x 393x 991x 991x 1x 1x 1264x 1264x 1264x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 597x 597x 597x                                             597x 597x  
/**
 * The pino dialect, translated into the emitter's record model.
 *
 * The emitter's logger takes a message *and* a field bag; pino's call sites take
 * them in either order, accept a bare string, an `Error`, or a bag alone, and lift
 * nothing out of any of them. The emitter lifts `err`, `req`, `res`, `messageId`
 * and `operation` out of the bag into slots of their own. Every shape a pino-shaped
 * caller actually uses is therefore translated here, in one place, rather than at
 * each call site:
 *
 * 1. `(obj, 'text')` — pino's canonical form.
 * 2. `(error)` — an `Error` as the only argument.
 * 3. `(obj)` — a bag with no text.
 * 4. `('text')` — a bare message.
 *
 * A bag's `message`/`msg` becomes the record's message, and its `operation` and
 * `messageId` are kept as the slots they already are, so a caller that speaks
 * pino gets the header it expects without changing. Everything else is carried
 * through as fields, untouched.
 *
 * A runtime with an envelope of its own — something that turns a *structured* log
 * call into a message and operation — layers on top of this, because that envelope
 * is the runtime's vocabulary and this module deliberately knows none: see
 * `@feasibleone/blong-gogo`'s `semanticRecord.ts` for one that lifts `$meta`.
 */
 
/** A record's message and the field bag it carries, as the emitter wants them. */
export interface LogCall {
    msg: string;
    fields: Record<string, unknown>;
}
 
/**
 * Is this a field bag? An array, an `Error`, `null` and any scalar are not: pino
 * accepts all of them in the bag's position, and none of them is a set of named
 * fields to merge.
 */
function isBag(value: unknown): value is Record<string, unknown> {
    return (
        typeof value === 'object' &&
        value !== null &&
        !Array.isArray(value) &&
        !(value instanceof Error)
    );
}
 
/** Read a string property without letting a non-string through. */
function text(value: unknown): string | undefined {
    return typeof value === 'string' ? value : undefined;
}
 
/**
 * Translate one pino-shaped log call's arguments into a message and a field bag.
 *
 * Total by construction: a call with no arguments, or with a first argument that is
 * none of the shapes above, still produces a record — `String(first)` is the last
 * resort, so a caller that logs something unexpected gets a line rather than a
 * throw out of the logging path.
 */
export function toLogCall(args: unknown[]): LogCall {
    const [first, second] = args;
    const trailing = isBag(second) ? second : undefined;
    // pino's canonical order is `(bag, message)`, so a string in the second position
    // is the text that belongs beside the bag — not a second bag.
    const beside = text(second);

    if (typeof first === 'string') {
        return {msg: first, fields: {...trailing}};
    }

    if (first instanceof Error) {
        // `err` is one of the slots the emitter lifts, so an error logged as the
        // message still renders as an error block rather than as text.
        return {msg: beside ?? first.message, fields: {err: first, ...trailing}};
    }

    if (isBag(first)) {
        // `message`/`msg` become the record's message, so they are not fields too;
        // `operation` and `messageId` are already the slots the emitter lifts, and
        // are carried through untouched.
        const {message, msg, ...rest} = first;
        const fields: Record<string, unknown> = {...rest, ...trailing};
        return {msg: beside ?? text(message) ?? text(msg) ?? '', fields};
    }

    return {msg: beside ?? (first === undefined ? '' : String(first)), fields: {...trailing}};
}