@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.
- package/README.md +90 -0
- package/dist/console-redirect.d.ts +5 -0
- package/dist/console-redirect.d.ts.map +1 -0
- package/dist/console-redirect.js +37 -0
- package/dist/console-redirect.js.map +1 -0
- package/dist/fatal-handlers.d.ts +21 -0
- package/dist/fatal-handlers.d.ts.map +1 -0
- package/dist/fatal-handlers.js +185 -0
- package/dist/fatal-handlers.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +36 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +176 -0
- package/dist/logger.js.map +1 -0
- package/dist/options.d.ts +32 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +25 -0
- package/dist/options.js.map +1 -0
- package/package.json +35 -0
- package/src/console-redirect.ts +50 -0
- package/src/fatal-handlers.ts +232 -0
- package/src/index.ts +34 -0
- package/src/logger.ts +255 -0
- package/src/options.ts +153 -0
|
@@ -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"}
|
package/dist/options.js
ADDED
|
@@ -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
|
+
};
|