@gyramais/log-transport 0.1.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.
@@ -0,0 +1,32 @@
1
+ export interface LokiTransportLike {
2
+ log?: unknown;
3
+ flush?: () => Promise<void>;
4
+ close?: () => void;
5
+ once?: (event: string, listener: (...args: unknown[]) => void) => unknown;
6
+ off?: (event: string, listener: (...args: unknown[]) => void) => unknown;
7
+ }
8
+ export interface LokiOptions {
9
+ host: string;
10
+ basicAuth?: string;
11
+ interval?: number;
12
+ timeout?: number;
13
+ labels?: Record<string, string>;
14
+ }
15
+ export interface FileOptions {
16
+ filename: string;
17
+ }
18
+ export interface LoggerOptions {
19
+ service: string;
20
+ app?: string;
21
+ level?: string;
22
+ instance?: string;
23
+ pretty?: boolean;
24
+ loki?: LokiOptions;
25
+ lokiTransport?: LokiTransportLike;
26
+ file?: FileOptions;
27
+ }
28
+ export declare const DEFAULT_APP_LABEL = "saas";
29
+ export declare const DEFAULT_LEVEL = "debug";
30
+ export declare const DEFAULT_LOKI_INTERVAL_SECONDS = 5;
31
+ export declare const DEFAULT_LOKI_TIMEOUT_MS = 60000;
32
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,iBAAiB;IAahC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;IAC1E,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;CAC1E;AAED,MAAM,WAAW,WAAW;IAE1B,IAAI,EAAE,MAAM,CAAC;IAEb,SAAS,CAAC,EAAE,MAAM,CAAC;IAMnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,OAAO,CAAC,EAAE,MAAM,CAAC;IAOjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAS5B,OAAO,EAAE,MAAM,CAAC;IAOhB,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,KAAK,CAAC,EAAE,MAAM,CAAC;IAQf,QAAQ,CAAC,EAAE,MAAM,CAAC;IASlB,MAAM,CAAC,EAAE,OAAO,CAAC;IAUjB,IAAI,CAAC,EAAE,WAAW,CAAC;IAkBnB,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAMlC,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,eAAO,MAAM,iBAAiB,SAAS,CAAC;AACxC,eAAO,MAAM,aAAa,UAAU,CAAC;AAGrC,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,eAAO,MAAM,uBAAuB,QAAQ,CAAC"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /*
3
+ Contrato de configuração do pacote.
4
+
5
+ Tudo entra por parâmetro: o pacote NÃO lê `process.env` em nenhum ponto. São três
6
+ razões, em ordem de peso:
7
+
8
+ 1. Correção. Os 15 serviços não leem os mesmos envs — `gyra-export` lê `APP_NAME`,
9
+ e `gyra-file` e `gyra-websocket-server` não leem `ENVIRONMENT` nem `LOG_LEVEL`.
10
+ Um helper de env compartilhado mudaria o comportamento desses em silêncio.
11
+ 2. Testabilidade. Ler env obriga o teste a mutar estado global do processo, o que
12
+ torna a suíte sensível à ordem de execução.
13
+ 3. Contrato visível. A assinatura diz o que o pacote precisa, sem o consumidor
14
+ ter que ler o código-fonte.
15
+
16
+ Quem lê env é o serviço, no entrypoint, onde o env já mora hoje.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.DEFAULT_LOKI_TIMEOUT_MS = exports.DEFAULT_LOKI_INTERVAL_SECONDS = exports.DEFAULT_LEVEL = exports.DEFAULT_APP_LABEL = void 0;
20
+ exports.DEFAULT_APP_LABEL = 'saas';
21
+ exports.DEFAULT_LEVEL = 'debug';
22
+ /* Curto de propósito — ver o comentário de `interval` acima. */
23
+ exports.DEFAULT_LOKI_INTERVAL_SECONDS = 5;
24
+ exports.DEFAULT_LOKI_TIMEOUT_MS = 60000;
25
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;EAeE;;;AAmIW,QAAA,iBAAiB,GAAG,MAAM,CAAC;AAC3B,QAAA,aAAa,GAAG,OAAO,CAAC;AAErC,gEAAgE;AACnD,QAAA,6BAA6B,GAAG,CAAC,CAAC;AAElC,QAAA,uBAAuB,GAAG,KAAK,CAAC"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@gyramais/log-transport",
3
+ "version": "0.1.0",
4
+ "description": "Entrega de log ao Loki e registro de excecao fatal para os servicos gyra-*",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "src"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/gyramais/gyra-libs.git",
17
+ "directory": "packages/log-transport"
18
+ },
19
+ "license": "ISC",
20
+ "scripts": {
21
+ "build": "rm -rf dist && tsc -p tsconfig.json && node ../../scripts/verify-build.js",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "jest --randomize",
24
+ "test:cov": "jest --coverage",
25
+ "prepack": "npm run build"
26
+ },
27
+ "dependencies": {
28
+ "winston-loki": "^6.1.6"
29
+ },
30
+ "peerDependencies": {
31
+ "@nestjs/common": ">=10",
32
+ "nest-winston": "^1.9.4",
33
+ "winston": "^3.13.1"
34
+ }
35
+ }
@@ -0,0 +1,50 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ /*
4
+ Redireciona `console.*` para o Logger do Nest, para que biblioteca de terceiro (e
5
+ código legado) que escreve em `console` também chegue aos transports.
6
+
7
+ Saiu do `buildNestLogger` de propósito: trocar os métodos de `console` é efeito
8
+ colateral global do processo, e importar um pacote não deve mudar o que
9
+ `console.log` faz no serviço inteiro. Quem quer, chama.
10
+ */
11
+
12
+ import { Logger } from '@nestjs/common';
13
+
14
+ export interface ConsoleRedirectOptions {
15
+ /*
16
+ Em formato legível (desenvolvimento), `console.log` vira `Logger.log` e aparece
17
+ no terminal. Fora dele vira `Logger.debug`, para não inflar o volume de
18
+ ingestão com o que bibliotecas de terceiro escrevem — o que já é problema
19
+ conhecido de custo no Loki.
20
+ */
21
+ pretty?: boolean;
22
+ }
23
+
24
+ export const redirectConsoleToNestLogger = (
25
+ options: ConsoleRedirectOptions = {},
26
+ ): void => {
27
+ console.log = (message?: any, ...optionalParams: any[]) => {
28
+ if (options.pretty) {
29
+ Logger.log(message, ...optionalParams);
30
+ } else {
31
+ Logger.debug(message, ...optionalParams);
32
+ }
33
+ };
34
+
35
+ console.error = (message?: any, data?: any, ...optionalParams: any[]) => {
36
+ Logger.error(message, { data }, ...optionalParams);
37
+ };
38
+
39
+ console.warn = (message?: any, ...optionalParams: any[]) => {
40
+ Logger.warn(message, ...optionalParams);
41
+ };
42
+
43
+ console.debug = (message?: any, ...optionalParams: any[]) => {
44
+ Logger.debug(message, ...optionalParams);
45
+ };
46
+
47
+ console.info = (message?: any, ...optionalParams: any[]) => {
48
+ Logger.verbose(message, ...optionalParams);
49
+ };
50
+ };
@@ -0,0 +1,232 @@
1
+ import { LoggerService } from '@nestjs/common';
2
+ import LokiTransport = require('winston-loki');
3
+
4
+ /*
5
+ O flush no caminho do crash é limitado no tempo de propósito: um Loki
6
+ indisponível não pode impedir o pod de morrer e reiniciar.
7
+ */
8
+ const FLUSH_TIMEOUT_MS = 2000;
9
+
10
+ type FatalOrigin = 'uncaughtException' | 'unhandledRejection';
11
+
12
+ interface FatalHandlersOptions {
13
+ flushTimeoutMs?: number;
14
+ exit?: (code: number) => void;
15
+ }
16
+
17
+ /*
18
+ Winston entrega a linha ao transport por stream, num tick posterior ao
19
+ `logger.error()`. Sem esperar por essa entrega, o batch ainda está vazio quando
20
+ o flush é pedido — e aí o flush resolve na hora e o processo sai levando a linha
21
+ embora. O transport emite `logged` quando recebe.
22
+ */
23
+ const LOGGED_HANDOFF_TIMEOUT_MS = 100;
24
+
25
+ const waitForHandoff = (
26
+ transport: LokiTransport,
27
+ timeoutMs: number,
28
+ ): Promise<void> =>
29
+ new Promise((resolve) => {
30
+ const done = () => {
31
+ clearTimeout(timer);
32
+ transport.off?.('logged', done);
33
+ resolve();
34
+ };
35
+ const timer = setTimeout(done, timeoutMs);
36
+ transport.once?.('logged', done);
37
+ });
38
+
39
+ /**
40
+ * Entrega ao Loki o que está bufferizado e só então devolve o controle,
41
+ * desistindo depois de `timeoutMs`. Nunca lança: no caminho de saída do
42
+ * processo, uma falha de flush não pode virar um segundo erro.
43
+ *
44
+ * O `flush()` do winston-loki apenas *aguarda* o laço periódico de envio, que
45
+ * pode estar a um intervalo inteiro de distância; quem está encerrando precisa
46
+ * *forçar* o envio agora. `close()` faz exatamente isso — interrompe a espera do
47
+ * laço e manda o batch pendente — e o `flush()` seguinte resolve quando esse
48
+ * envio termina.
49
+ */
50
+ const flushLokiTransport = async (
51
+ transport: LokiTransport | undefined,
52
+ timeoutMs = FLUSH_TIMEOUT_MS,
53
+ ): Promise<void> => {
54
+ if (!transport?.flush) return;
55
+
56
+ let timer: NodeJS.Timeout | undefined;
57
+
58
+ const drain = async () => {
59
+ await waitForHandoff(
60
+ transport,
61
+ Math.min(LOGGED_HANDOFF_TIMEOUT_MS, timeoutMs),
62
+ );
63
+ transport.close?.();
64
+ await transport.flush();
65
+ };
66
+
67
+ try {
68
+ await Promise.race([
69
+ drain(),
70
+ new Promise<void>((resolve) => {
71
+ timer = setTimeout(resolve, timeoutMs);
72
+ }),
73
+ ]);
74
+ } catch {
75
+ /*
76
+ Silencioso por escolha: quem chama já está encerrando o processo e a linha
77
+ de erro do flush não teria para onde ir.
78
+ */
79
+ } finally {
80
+ if (timer) clearTimeout(timer);
81
+ }
82
+ };
83
+
84
+ const describeReason = (
85
+ reason: unknown,
86
+ ): { message: string; stack: string } => {
87
+ if (reason instanceof Error) {
88
+ return {
89
+ message: `${reason.name}: ${reason.message}`,
90
+ stack: reason.stack ?? '',
91
+ };
92
+ }
93
+
94
+ return { message: String(reason), stack: '' };
95
+ };
96
+
97
+ /**
98
+ * Registra os handlers de exceção fatal do processo.
99
+ *
100
+ * Sem eles, o handler padrão do Node imprime a stack direto no stderr — sem
101
+ * passar por nenhum transport do winston — e o crash fica invisível na
102
+ * observabilidade, encontrável apenas por `kubectl logs --previous`.
103
+ *
104
+ * O processo continua morrendo: depois de uma exceção não capturada o estado é
105
+ * indefinido, e reiniciar é o comportamento correto. O que muda é o crash passar
106
+ * a ficar registrado.
107
+ */
108
+ /*
109
+ Handler de exceção fatal é recurso ÚNICO do processo, e o guard de reentrância
110
+ vive no closure de cada chamada — então dois registros são dois guards, e um
111
+ único erro fatal dispara os dois. Medido: quatro registros produziram quatro
112
+ linhas `[FATAL]`, quatro flushes concorrentes no mesmo transport e quatro
113
+ chamadas de exit.
114
+
115
+ O risco cresceu quando o pacote passou a suportar vários loggers com um
116
+ transport compartilhado (ver `createLokiTransport`): fica natural registrar um
117
+ handler por logger.
118
+
119
+ Este é o único estado global do pacote, e a distinção importa: ele descreve o
120
+ PROCESSO — se os handlers já estão instalados nele —, não configuração. Config
121
+ em variável global é o que `options.ts` recusa; isto é outra coisa.
122
+
123
+ `Symbol.for` e não um `let` de módulo: sobrevive a duas instâncias do pacote na
124
+ árvore de dependências, que é exatamente o caso em que o registro duplicado
125
+ aconteceria sem ninguém perceber.
126
+ */
127
+ const REGISTERED = Symbol.for(
128
+ '@gyramais/log-transport.fatalHandlersRegistered',
129
+ );
130
+
131
+ type GlobalWithGuard = typeof globalThis & { [REGISTERED]?: boolean };
132
+
133
+ const registerFatalHandlers = (
134
+ logger: LoggerService,
135
+ transport: LokiTransport | undefined,
136
+ options: FatalHandlersOptions = {},
137
+ ): (() => void) => {
138
+ const target = globalThis as GlobalWithGuard;
139
+
140
+ if (target[REGISTERED]) {
141
+ /*
142
+ Avisa em vez de ignorar em silêncio: registro duplicado é erro de
143
+ integração do consumidor, e descobrir isso durante um crash em produção é
144
+ o pior momento possível.
145
+ */
146
+ process.stdout.write(
147
+ `${JSON.stringify({
148
+ level: 'warn',
149
+ message:
150
+ '[FATAL-HANDLERS-DUP] registerFatalHandlers chamado mais de uma vez ' +
151
+ 'neste processo; a chamada extra foi ignorada. Chame uma vez, com o ' +
152
+ 'transport compartilhado.',
153
+ timestamp: new Date().toISOString(),
154
+ })}\n`,
155
+ );
156
+
157
+ return () => undefined;
158
+ }
159
+
160
+ target[REGISTERED] = true;
161
+ const flushTimeoutMs = options.flushTimeoutMs ?? FLUSH_TIMEOUT_MS;
162
+ const exit = options.exit ?? ((code: number) => process.exit(code));
163
+
164
+ /*
165
+ Um segundo erro fatal enquanto o primeiro ainda está sendo flushado não pode
166
+ reiniciar o ciclo — senão o processo nunca chega ao exit.
167
+ */
168
+ let handling = false;
169
+
170
+ const handleFatal = async (origin: FatalOrigin, reason: unknown) => {
171
+ if (handling) return;
172
+ handling = true;
173
+
174
+ /*
175
+ O exit vai no finally porque o guard de reentrância já está levantado: se
176
+ registrar ou flushar falhasse aqui, uma segunda passagem sairia na primeira
177
+ linha e o processo ficaria pendurado — travado num estado indefinido em vez
178
+ de reiniciar.
179
+ */
180
+ try {
181
+ const { message, stack } = describeReason(reason);
182
+
183
+ logger.error(`[FATAL] ${origin}: ${message}`, {
184
+ origin,
185
+ /*
186
+ String, não array: o Loki exige valores string na structured metadata e
187
+ rejeita a requisição inteira quando recebe outra coisa.
188
+ */
189
+ stack,
190
+ });
191
+
192
+ await flushLokiTransport(transport, flushTimeoutMs);
193
+ } finally {
194
+ exit(1);
195
+ }
196
+ };
197
+
198
+ /*
199
+ O catch fecha o laço: sem ele, uma falha dentro do handler de
200
+ unhandledRejection viraria uma nova unhandledRejection.
201
+ */
202
+ const onFatal =
203
+ (origin: FatalOrigin) =>
204
+ (reason: unknown): void => {
205
+ handleFatal(origin, reason).catch((): void => {
206
+ /*
207
+ Vazio de propósito: o processo já está encerrando, e uma linha de erro
208
+ daqui não teria para onde ir.
209
+ */
210
+ });
211
+ };
212
+
213
+ const onUncaught = onFatal('uncaughtException');
214
+ const onRejection = onFatal('unhandledRejection');
215
+
216
+ process.on('uncaughtException', onUncaught);
217
+ process.on('unhandledRejection', onRejection);
218
+
219
+ /*
220
+ Desfaz o registro e libera o guard. Em produção o processo não desregistra —
221
+ ele morre. Isto existe para o teste poder registrar de novo na próxima
222
+ execução, e é o que torna o guard acima verificável em vez de um efeito
223
+ global impossível de exercitar.
224
+ */
225
+ return () => {
226
+ process.off('uncaughtException', onUncaught);
227
+ process.off('unhandledRejection', onRejection);
228
+ delete target[REGISTERED];
229
+ };
230
+ };
231
+
232
+ export { registerFatalHandlers, flushLokiTransport };
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ /*
2
+ Entrega de log dos serviços gyra-*: monta os transports (Console, File, Loki) a
3
+ partir de configuração explícita, e registra exceção fatal com flush antes do
4
+ exit.
5
+
6
+ Nada aqui lê variável de ambiente — ver `options.ts` para o porquê. Quem lê é o
7
+ serviço, no entrypoint dele.
8
+
9
+ O exemplo de uso completo está no README do pacote. Ele NÃO fica aqui de
10
+ propósito: o exemplo mostra o consumidor lendo `process.env`, e como o build
11
+ publica os comentários (`removeComments: false`), essas linhas apareceriam no
12
+ `dist/` e fariam um grep de auditoria acusar o pacote de ler ambiente.
13
+ */
14
+
15
+ export { buildNestLogger, createLokiTransport } from './logger';
16
+ export type { BuiltLogger, CreateLokiTransportOptions } from './logger';
17
+
18
+ export { redirectConsoleToNestLogger } from './console-redirect';
19
+ export type { ConsoleRedirectOptions } from './console-redirect';
20
+
21
+ export { registerFatalHandlers, flushLokiTransport } from './fatal-handlers';
22
+
23
+ export {
24
+ DEFAULT_APP_LABEL,
25
+ DEFAULT_LEVEL,
26
+ DEFAULT_LOKI_INTERVAL_SECONDS,
27
+ DEFAULT_LOKI_TIMEOUT_MS,
28
+ } from './options';
29
+ export type {
30
+ LoggerOptions,
31
+ LokiOptions,
32
+ FileOptions,
33
+ LokiTransportLike,
34
+ } from './options';
package/src/logger.ts ADDED
@@ -0,0 +1,255 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ /*
4
+ PROVENIÊNCIA: derivado de gyra-core/src/common/logger/LoggerFactory.ts na branch
5
+ GYR-1555 (tag `backup/GYR-1555-pre-close`), onde o defeito de entrega ao Loki foi
6
+ diagnosticado e provado contra um Loki 3.0 real: antes, 21 linhas enviadas e 0
7
+ chegando, sem nenhum erro reportado; depois, 21 de 21.
8
+
9
+ Diferenças em relação à fonte, todas deliberadas:
10
+
11
+ - Nenhuma leitura de `process.env` — ver `options.ts`.
12
+ - Sem singleton de módulo. A fonte guardava o transport numa variável de módulo
13
+ para o handler de exceção fatal alcançá-lo; aqui ele volta no retorno, porque
14
+ estado global no pacote é o mesmo problema de ler env com outra roupa.
15
+
16
+ O singleton da fonte, porém, resolvia um problema real que o retorno sozinho
17
+ não resolve: serviço que monta VÁRIOS loggers no mesmo processo — o worker do
18
+ gyra-core monta quatro — teria um batcher por logger, e o flush de exceção
19
+ fatal drenaria só um deles. Para isso existe `createLokiTransport`: o
20
+ consumidor cria o transport uma vez e passa o mesmo em `lokiTransport`. O
21
+ compartilhamento fica explícito no call site, em vez de escondido no módulo.
22
+ - O redirecionamento de `console.*` saiu para `console-redirect.ts`, como opt-in
23
+ explícito: é efeito colateral de processo, e importar um pacote não deve mudar
24
+ o que `console.log` faz.
25
+ */
26
+
27
+ import { Logform, format, transports } from 'winston';
28
+ import { WinstonModule } from 'nest-winston';
29
+ import LokiTransport = require('winston-loki');
30
+ import type { LoggerService } from '@nestjs/common';
31
+ import { hostname } from 'os';
32
+
33
+ import {
34
+ DEFAULT_APP_LABEL,
35
+ DEFAULT_LEVEL,
36
+ DEFAULT_LOKI_INTERVAL_SECONDS,
37
+ DEFAULT_LOKI_TIMEOUT_MS,
38
+ LoggerOptions,
39
+ LokiOptions,
40
+ } from './options';
41
+
42
+ /*
43
+ O winston-loki só reportava erro de conexão; a 6.1.6 passou a anexar o status
44
+ HTTP quando o Loki recusa o envio.
45
+ */
46
+ type LokiShipError = Error & { statusCode?: number };
47
+
48
+ export interface BuiltLogger {
49
+ logger: LoggerService;
50
+
51
+ /*
52
+ Presente somente quando `options.loki` foi informado. Quem trata exceção fatal
53
+ precisa dele para dar flush antes do exit — sem isso a stack do crash nunca sai
54
+ do buffer.
55
+ */
56
+ lokiTransport?: LokiTransport;
57
+ }
58
+
59
+ const formatMeta = (meta: Record<string | symbol, any>): string => {
60
+ const splat = meta[Symbol.for('splat')];
61
+
62
+ if (splat?.[0].stack) {
63
+ return ' ' + JSON.stringify(splat?.[0].stack?.[0], null, 2);
64
+ }
65
+
66
+ if (splat && splat.length) {
67
+ const obj = splat?.[0]?.context ?? splat;
68
+
69
+ if (typeof obj === 'string') {
70
+ return ' ' + obj;
71
+ }
72
+
73
+ if (
74
+ Array.isArray(obj) &&
75
+ obj.length === 1 &&
76
+ typeof obj[0] === 'object' &&
77
+ Object.keys(obj[0]).length === 1 &&
78
+ obj[0].context === undefined
79
+ ) {
80
+ return '';
81
+ }
82
+
83
+ return ' ' + JSON.stringify(obj, null, 2);
84
+ }
85
+
86
+ return '';
87
+ };
88
+
89
+ const buildPrettyFormat = (): Logform.Format => {
90
+ const localFormat = format.printf(({ message, level, ..._meta }) => {
91
+ const color = {
92
+ white: '\x1B[38;2;173;190;203m',
93
+ blue: '\x1B[34m',
94
+ red: '\x1B[31m',
95
+ yellow: '\x1B[33m',
96
+ cyan: '\x1B[36m',
97
+ magenta: '\x1B[35m',
98
+ reset: '\x1B[0m',
99
+ };
100
+
101
+ const levelColor: Record<string, string> = {
102
+ info: color.white,
103
+ error: color.red,
104
+ warn: color.yellow,
105
+ debug: color.cyan,
106
+ verbose: color.magenta,
107
+ };
108
+
109
+ const currentColor = levelColor[level];
110
+
111
+ const meta = formatMeta(_meta);
112
+
113
+ /*
114
+ `verbose` é o maior level, com 7 letras; +1 para sempre sobrar espaço.
115
+ */
116
+ let finalMessage = `${level.padEnd(8)} ${message}${meta ?? ''}`;
117
+ finalMessage = finalMessage.replaceAll('\n', `\n${currentColor ?? 'red'}`);
118
+
119
+ return `${currentColor}` + finalMessage + `${color.reset}`;
120
+ });
121
+
122
+ return format.combine(format.splat(), localFormat);
123
+ };
124
+
125
+ export interface CreateLokiTransportOptions extends LokiOptions {
126
+ service: string;
127
+ app?: string;
128
+ instance?: string;
129
+ pretty?: boolean;
130
+ }
131
+
132
+ /**
133
+ * Constrói o transport do Loki isoladamente, para o consumidor COMPARTILHAR um
134
+ * único batcher entre vários loggers do mesmo processo.
135
+ *
136
+ * Use isto quando o serviço monta mais de um logger — o worker do gyra-core monta
137
+ * quatro. Com um transport por logger, cada um tem seu batcher, e o flush de
138
+ * exceção fatal cobre só um deles.
139
+ *
140
+ * const lokiTransport = createLokiTransport({ service: 'gyra-core', host });
141
+ * const { logger } = buildNestLogger({ service: 'gyra-core', lokiTransport });
142
+ * // ...e o mesmo `lokiTransport` nos outros loggers do processo
143
+ */
144
+ export const createLokiTransport = (
145
+ options: CreateLokiTransportOptions,
146
+ ): LokiTransport => {
147
+ const { service, app, instance, pretty, ...loki } = options;
148
+
149
+ return buildLokiTransport(
150
+ loki,
151
+ {
152
+ service,
153
+ app: app ?? DEFAULT_APP_LABEL,
154
+ instance: instance ?? hostname(),
155
+ ...loki.labels,
156
+ },
157
+ pretty
158
+ ? buildPrettyFormat()
159
+ : format.combine(format.timestamp(), format.json()),
160
+ );
161
+ };
162
+
163
+ const buildLokiTransport = (
164
+ loki: LokiOptions,
165
+ labels: Record<string, string>,
166
+ lineFormat: Logform.Format,
167
+ ): LokiTransport =>
168
+ new LokiTransport({
169
+ host: loki.host,
170
+ basicAuth: loki.basicAuth,
171
+ json: true,
172
+ labels,
173
+ interval: loki.interval ?? DEFAULT_LOKI_INTERVAL_SECONDS,
174
+ batching: true,
175
+ timeout: loki.timeout ?? DEFAULT_LOKI_TIMEOUT_MS,
176
+ format: lineFormat,
177
+
178
+ /*
179
+ A falha de entrega tem que ser audível: enquanto era silenciosa, o serviço
180
+ perdeu ~99,5% dos seus logs sem ninguém notar. Escreve direto no console
181
+ porque passar pelo logger completo realimentaria o Loki num laço de erro.
182
+ */
183
+ onConnectionError: (err: LokiShipError) =>
184
+ process.stdout.write(
185
+ `${JSON.stringify({
186
+ level: 'error',
187
+ message: `[LOKI-SHIP-FAIL] status=${err?.statusCode ?? 'none'} ${
188
+ err?.message ?? err
189
+ }`,
190
+ timestamp: new Date().toISOString(),
191
+ })}\n`,
192
+ ),
193
+ });
194
+
195
+ /**
196
+ * Monta o logger do Nest com os transports pedidos e devolve, junto, o transport
197
+ * do Loki — necessário para `registerFatalHandlers` dar flush antes do exit.
198
+ *
199
+ * Não instala nada no processo: nem handler de exceção, nem redirecionamento de
200
+ * `console.*`. Esses são opt-in explícito, porque mexem no ciclo de vida do
201
+ * processo.
202
+ */
203
+ export const buildNestLogger = (options: LoggerOptions): BuiltLogger => {
204
+ const lineFormat = options.pretty
205
+ ? buildPrettyFormat()
206
+ : format.combine(format.timestamp(), format.json());
207
+
208
+ /*
209
+ Passar os dois é ambíguo: um pede para construir um transport novo, o outro
210
+ para reusar um existente. Falhar alto é melhor que escolher em silêncio e
211
+ deixar o consumidor com dois batchers sem saber.
212
+ */
213
+ if (options.loki && options.lokiTransport) {
214
+ throw new Error(
215
+ '[log-transport] `loki` e `lokiTransport` sao mutuamente exclusivos: ' +
216
+ 'use `loki` para construir um transport novo, ou `lokiTransport` para ' +
217
+ 'reusar um ja construido por createLokiTransport().',
218
+ );
219
+ }
220
+
221
+ const transportsList: any[] = [
222
+ new transports.Console({ format: lineFormat }),
223
+ ];
224
+
225
+ let lokiTransport: LokiTransport | undefined;
226
+
227
+ if (options.lokiTransport) {
228
+ lokiTransport = options.lokiTransport as LokiTransport;
229
+ transportsList.push(lokiTransport);
230
+ } else if (options.loki) {
231
+ lokiTransport = buildLokiTransport(
232
+ options.loki,
233
+ {
234
+ service: options.service,
235
+ app: options.app ?? DEFAULT_APP_LABEL,
236
+ instance: options.instance ?? hostname(),
237
+ ...options.loki.labels,
238
+ },
239
+ lineFormat,
240
+ );
241
+
242
+ transportsList.push(lokiTransport);
243
+ } else if (options.file) {
244
+ transportsList.push(
245
+ new transports.File({ filename: options.file.filename }),
246
+ );
247
+ }
248
+
249
+ const logger = WinstonModule.createLogger({
250
+ level: options.level ?? DEFAULT_LEVEL,
251
+ transports: transportsList,
252
+ });
253
+
254
+ return { logger, lokiTransport };
255
+ };