@growconnect/growtickets-nest 0.1.0 → 0.3.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/.env.example +8 -0
- package/README.md +319 -6
- package/dist/cli/args.d.ts +9 -1
- package/dist/cli/args.js +13 -4
- package/dist/cli/check.d.ts +4 -1
- package/dist/cli/check.js +47 -2
- package/dist/cli/env-example.js +17 -0
- package/dist/cli/env.d.ts +34 -0
- package/dist/cli/env.js +125 -0
- package/dist/cli/index.js +35 -1
- package/dist/cli/init.js +5 -1
- package/dist/client/ingest-client.js +6 -1
- package/dist/env/applier.d.ts +83 -0
- package/dist/env/applier.js +162 -0
- package/dist/env/errors.d.ts +12 -0
- package/dist/env/errors.js +18 -0
- package/dist/env/load.d.ts +35 -0
- package/dist/env/load.js +43 -0
- package/dist/env/overlay.d.ts +23 -0
- package/dist/env/overlay.js +48 -0
- package/dist/env/policy.d.ts +30 -0
- package/dist/env/policy.js +101 -0
- package/dist/env/poller.d.ts +27 -0
- package/dist/env/poller.js +54 -0
- package/dist/env/runtime.d.ts +122 -0
- package/dist/env/runtime.js +238 -0
- package/dist/env/settings.d.ts +26 -0
- package/dist/env/settings.js +41 -0
- package/dist/env/snapshot.d.ts +32 -0
- package/dist/env/snapshot.js +85 -0
- package/dist/env/state.d.ts +27 -0
- package/dist/env/state.js +51 -0
- package/dist/env/storage.d.ts +33 -0
- package/dist/env/storage.js +133 -0
- package/dist/env/transport/client.d.ts +58 -0
- package/dist/env/transport/client.js +249 -0
- package/dist/env/transport/config.d.ts +20 -0
- package/dist/env/transport/config.js +38 -0
- package/dist/env/transport/credential.d.ts +33 -0
- package/dist/env/transport/credential.js +76 -0
- package/dist/env/transport/gt2-transport.d.ts +53 -0
- package/dist/env/transport/gt2-transport.js +162 -0
- package/dist/env/transport/index.d.ts +10 -0
- package/dist/env/transport/index.js +17 -0
- package/dist/env/transport/keygen.d.ts +32 -0
- package/dist/env/transport/keygen.js +77 -0
- package/dist/env/transport/nonce-cache.d.ts +18 -0
- package/dist/env/transport/nonce-cache.js +37 -0
- package/dist/env/transport/protocol.d.ts +100 -0
- package/dist/env/transport/protocol.js +292 -0
- package/dist/env/transport/types.d.ts +78 -0
- package/dist/env/transport/types.js +2 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +18 -1
- package/dist/logs/buffer.d.ts +25 -0
- package/dist/logs/buffer.js +58 -0
- package/dist/logs/doctor.d.ts +17 -0
- package/dist/logs/doctor.js +66 -0
- package/dist/logs/entry-builder.d.ts +20 -0
- package/dist/logs/entry-builder.js +127 -0
- package/dist/logs/flusher.d.ts +72 -0
- package/dist/logs/flusher.js +189 -0
- package/dist/logs/format.d.ts +4 -0
- package/dist/logs/format.js +76 -0
- package/dist/logs/levels.d.ts +12 -0
- package/dist/logs/levels.js +55 -0
- package/dist/logs/monitor.d.ts +92 -0
- package/dist/logs/monitor.js +224 -0
- package/dist/logs/normalize.d.ts +14 -0
- package/dist/logs/normalize.js +114 -0
- package/dist/logs/pino-line.d.ts +13 -0
- package/dist/logs/pino-line.js +52 -0
- package/dist/logs/redactor.d.ts +38 -0
- package/dist/logs/redactor.js +146 -0
- package/dist/logs/remote-config.d.ts +37 -0
- package/dist/logs/remote-config.js +89 -0
- package/dist/logs/runtime.d.ts +24 -0
- package/dist/logs/runtime.js +59 -0
- package/dist/logs/sampler.d.ts +15 -0
- package/dist/logs/sampler.js +36 -0
- package/dist/logs/sender.d.ts +20 -0
- package/dist/logs/sender.js +64 -0
- package/dist/logs/settings.d.ts +22 -0
- package/dist/logs/settings.js +30 -0
- package/dist/logs/types.d.ts +74 -0
- package/dist/logs/types.js +9 -0
- package/dist/logs-nest/api.d.ts +5 -0
- package/dist/logs-nest/api.js +13 -0
- package/dist/logs-nest/host.d.ts +10 -0
- package/dist/logs-nest/host.js +45 -0
- package/dist/logs-nest/lifecycle.d.ts +20 -0
- package/dist/logs-nest/lifecycle.js +40 -0
- package/dist/logs-nest/nest-args.d.ts +7 -0
- package/dist/logs-nest/nest-args.js +66 -0
- package/dist/logs-nest/service.d.ts +25 -0
- package/dist/logs-nest/service.js +61 -0
- package/dist/logs-nest/stream.d.ts +21 -0
- package/dist/logs-nest/stream.js +32 -0
- package/dist/maintenance/feed.d.ts +68 -0
- package/dist/maintenance/feed.js +160 -0
- package/dist/maintenance/http.d.ts +67 -0
- package/dist/maintenance/http.js +177 -0
- package/dist/maintenance/page.d.ts +11 -0
- package/dist/maintenance/page.js +208 -0
- package/dist/maintenance/settings.d.ts +24 -0
- package/dist/maintenance/settings.js +42 -0
- package/dist/maintenance/windows.d.ts +78 -0
- package/dist/maintenance/windows.js +167 -0
- package/dist/nest/csp-response.d.ts +19 -0
- package/dist/nest/csp-response.js +84 -0
- package/dist/nest/env-push.d.ts +35 -0
- package/dist/nest/env-push.js +110 -0
- package/dist/nest/growtickets.module.js +11 -3
- package/dist/nest/maintenance-request.d.ts +12 -0
- package/dist/nest/maintenance-request.js +30 -0
- package/dist/nest/maintenance.guard.d.ts +28 -0
- package/dist/nest/maintenance.guard.js +90 -0
- package/dist/nest/options.d.ts +15 -0
- package/dist/nest/request.d.ts +1 -1
- package/dist/nest/request.js +2 -2
- package/dist/nest/route-tag.d.ts +6 -0
- package/dist/nest/route-tag.js +13 -0
- package/dist/nest/security.middleware.d.ts +10 -4
- package/dist/nest/security.middleware.js +77 -18
- package/dist/nest/services.js +1 -1
- package/dist/proxy/handle-portal-request.js +2 -1
- package/dist/proxy/types.d.ts +1 -0
- package/dist/proxy/unread.js +1 -1
- package/dist/security/client-ip.d.ts +15 -1
- package/dist/security/client-ip.js +20 -2
- package/dist/security/csp.d.ts +58 -0
- package/dist/security/csp.js +109 -0
- package/dist/security/flusher.js +4 -2
- package/dist/security/proxy-chain.d.ts +35 -0
- package/dist/security/proxy-chain.js +58 -0
- package/dist/security/remote-config.d.ts +4 -0
- package/dist/security/remote-config.js +7 -0
- package/dist/security/request-ip.d.ts +3 -2
- package/dist/security/request-ip.js +6 -3
- package/dist/security/runtime.d.ts +8 -0
- package/dist/security/runtime.js +17 -0
- package/dist/security/sdk.d.ts +7 -1
- package/dist/security/sdk.js +27 -1
- package/dist/security/settings.d.ts +3 -1
- package/dist/security/settings.js +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,8 +11,9 @@ export type { GrowTicketsTrackOptions } from './nest/services';
|
|
|
11
11
|
export { GrowTicketsLogin, GrowTicketsOnSuccess, GrowTicketsSecondFactor } from './nest/decorators';
|
|
12
12
|
export type { GrowTicketsAccountOptions } from './nest/decorators';
|
|
13
13
|
export { GrowTicketsRouteGuard } from './nest/route-tag.guard';
|
|
14
|
+
export { GrowTicketsMaintenanceGuard } from './nest/maintenance.guard';
|
|
14
15
|
export { DEFAULT_PORTAL_PATH, DEFAULT_THROTTLE, GROWTICKETS_OPTIONS } from './nest/options';
|
|
15
|
-
export type { GrowTicketsGuard, GrowTicketsModuleAsyncOptions, GrowTicketsModuleOptions, GrowTicketsSecurityOptions } from './nest/options';
|
|
16
|
+
export type { GrowTicketsGuard, GrowTicketsMaintenanceOptions, GrowTicketsModuleAsyncOptions, GrowTicketsModuleOptions, GrowTicketsSecurityOptions } from './nest/options';
|
|
16
17
|
export type { PublicErrorBody, Reporter } from './contracts';
|
|
17
18
|
export { TICKET_REFERENCE } from './contracts';
|
|
18
19
|
export { SecretValue } from './secret-value';
|
|
@@ -30,3 +31,15 @@ export type { HandlePortalRequestOptions, PortalRequest, PortalResult } from './
|
|
|
30
31
|
export { PORTAL_ROUTES, matchPortalRoute, REFERENCE } from './proxy/portal-routes';
|
|
31
32
|
export type { PortalRoute, IdentityMode, MatchedPortalRoute } from './proxy/portal-routes';
|
|
32
33
|
export { SDK_NAME, SDK_VERSION } from './security/sdk';
|
|
34
|
+
export type { AppCspOptions } from './security/csp';
|
|
35
|
+
export { loadGrowTicketsEnv, onGrowTicketsEnvChange } from './env/load';
|
|
36
|
+
export type { EnvChange } from './env/applier';
|
|
37
|
+
export type { EnvChangeListener, LoadEnvOptions, LoadEnvResult, LoadEnvStatus } from './env/runtime';
|
|
38
|
+
export { EnvError } from './env/errors';
|
|
39
|
+
export { GrowTicketsLogService } from './logs-nest/service';
|
|
40
|
+
export { GrowTicketsLogsLifecycle, GrowTicketsLogsModule } from './logs-nest/lifecycle';
|
|
41
|
+
export { growTicketsLogStream } from './logs-nest/stream';
|
|
42
|
+
export type { GrowTicketsLogStream, GrowTicketsLogStreamOptions } from './logs-nest/stream';
|
|
43
|
+
export { flushGrowTicketsLogs, shutdownGrowTicketsLogs } from './logs-nest/api';
|
|
44
|
+
export type { FlushResult } from './logs/flusher';
|
|
45
|
+
export type { LogLevel, LogRecord } from './logs/types';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SDK_VERSION = exports.SDK_NAME = exports.REFERENCE = exports.matchPortalRoute = exports.PORTAL_ROUTES = exports.handleUnreadRequest = exports.handlePortalRequest = exports.createGrowTicketsClient = exports.countUnread = exports.createDefaultLogger = exports.redactByValue = exports.redactByKey = exports.statusForError = exports.toPublicError = exports.RateLimitError = exports.ExternalServiceError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.ConfigurationError = exports.GrowTicketsError = exports.getServerConfig = exports.SecretValue = exports.TICKET_REFERENCE = exports.GROWTICKETS_OPTIONS = exports.DEFAULT_THROTTLE = exports.DEFAULT_PORTAL_PATH = exports.GrowTicketsRouteGuard = exports.GrowTicketsSecondFactor = exports.GrowTicketsOnSuccess = exports.GrowTicketsLogin = exports.GrowTicketsSecurity = exports.GrowTicketsClient = exports.growTicketsMiddleware = exports.GrowTicketsModule = void 0;
|
|
3
|
+
exports.shutdownGrowTicketsLogs = exports.flushGrowTicketsLogs = exports.growTicketsLogStream = exports.GrowTicketsLogsModule = exports.GrowTicketsLogsLifecycle = exports.GrowTicketsLogService = exports.EnvError = exports.onGrowTicketsEnvChange = exports.loadGrowTicketsEnv = exports.SDK_VERSION = exports.SDK_NAME = exports.REFERENCE = exports.matchPortalRoute = exports.PORTAL_ROUTES = exports.handleUnreadRequest = exports.handlePortalRequest = exports.createGrowTicketsClient = exports.countUnread = exports.createDefaultLogger = exports.redactByValue = exports.redactByKey = exports.statusForError = exports.toPublicError = exports.RateLimitError = exports.ExternalServiceError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.ConfigurationError = exports.GrowTicketsError = exports.getServerConfig = exports.SecretValue = exports.TICKET_REFERENCE = exports.GROWTICKETS_OPTIONS = exports.DEFAULT_THROTTLE = exports.DEFAULT_PORTAL_PATH = exports.GrowTicketsMaintenanceGuard = exports.GrowTicketsRouteGuard = exports.GrowTicketsSecondFactor = exports.GrowTicketsOnSuccess = exports.GrowTicketsLogin = exports.GrowTicketsSecurity = exports.GrowTicketsClient = exports.growTicketsMiddleware = exports.GrowTicketsModule = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `@growconnect/growtickets-nest`: el proxy del portal de GrowTickets y la seguridad del producto para NestJS
|
|
6
6
|
* (Express o Fastify). Lo de Nest está en `./nest`; el resto es el núcleo agnóstico, el mismo de
|
|
@@ -19,6 +19,8 @@ Object.defineProperty(exports, "GrowTicketsOnSuccess", { enumerable: true, get:
|
|
|
19
19
|
Object.defineProperty(exports, "GrowTicketsSecondFactor", { enumerable: true, get: function () { return decorators_1.GrowTicketsSecondFactor; } });
|
|
20
20
|
var route_tag_guard_1 = require("./nest/route-tag.guard");
|
|
21
21
|
Object.defineProperty(exports, "GrowTicketsRouteGuard", { enumerable: true, get: function () { return route_tag_guard_1.GrowTicketsRouteGuard; } });
|
|
22
|
+
var maintenance_guard_1 = require("./nest/maintenance.guard");
|
|
23
|
+
Object.defineProperty(exports, "GrowTicketsMaintenanceGuard", { enumerable: true, get: function () { return maintenance_guard_1.GrowTicketsMaintenanceGuard; } });
|
|
22
24
|
var options_1 = require("./nest/options");
|
|
23
25
|
Object.defineProperty(exports, "DEFAULT_PORTAL_PATH", { enumerable: true, get: function () { return options_1.DEFAULT_PORTAL_PATH; } });
|
|
24
26
|
Object.defineProperty(exports, "DEFAULT_THROTTLE", { enumerable: true, get: function () { return options_1.DEFAULT_THROTTLE; } });
|
|
@@ -58,3 +60,18 @@ Object.defineProperty(exports, "REFERENCE", { enumerable: true, get: function ()
|
|
|
58
60
|
var sdk_1 = require("./security/sdk");
|
|
59
61
|
Object.defineProperty(exports, "SDK_NAME", { enumerable: true, get: function () { return sdk_1.SDK_NAME; } });
|
|
60
62
|
Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return sdk_1.SDK_VERSION; } });
|
|
63
|
+
var load_1 = require("./env/load");
|
|
64
|
+
Object.defineProperty(exports, "loadGrowTicketsEnv", { enumerable: true, get: function () { return load_1.loadGrowTicketsEnv; } });
|
|
65
|
+
Object.defineProperty(exports, "onGrowTicketsEnvChange", { enumerable: true, get: function () { return load_1.onGrowTicketsEnvChange; } });
|
|
66
|
+
var errors_2 = require("./env/errors");
|
|
67
|
+
Object.defineProperty(exports, "EnvError", { enumerable: true, get: function () { return errors_2.EnvError; } });
|
|
68
|
+
var service_1 = require("./logs-nest/service");
|
|
69
|
+
Object.defineProperty(exports, "GrowTicketsLogService", { enumerable: true, get: function () { return service_1.GrowTicketsLogService; } });
|
|
70
|
+
var lifecycle_1 = require("./logs-nest/lifecycle");
|
|
71
|
+
Object.defineProperty(exports, "GrowTicketsLogsLifecycle", { enumerable: true, get: function () { return lifecycle_1.GrowTicketsLogsLifecycle; } });
|
|
72
|
+
Object.defineProperty(exports, "GrowTicketsLogsModule", { enumerable: true, get: function () { return lifecycle_1.GrowTicketsLogsModule; } });
|
|
73
|
+
var stream_1 = require("./logs-nest/stream");
|
|
74
|
+
Object.defineProperty(exports, "growTicketsLogStream", { enumerable: true, get: function () { return stream_1.growTicketsLogStream; } });
|
|
75
|
+
var api_1 = require("./logs-nest/api");
|
|
76
|
+
Object.defineProperty(exports, "flushGrowTicketsLogs", { enumerable: true, get: function () { return api_1.flushGrowTicketsLogs; } });
|
|
77
|
+
Object.defineProperty(exports, "shutdownGrowTicketsLogs", { enumerable: true, get: function () { return api_1.shutdownGrowTicketsLogs; } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WireEntry } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Las líneas capturadas que esperan al siguiente envío, en memoria y con tope.
|
|
4
|
+
*
|
|
5
|
+
* Lo que llena el tope no tumba nada: una línea leve que llega con la cola llena se descarta y se cuenta como perdida; una grave
|
|
6
|
+
* (`warning` o peor) hace sitio quitando la leve más vieja, o la más vieja si todas son graves. Lo perdido viaja en el campo
|
|
7
|
+
* `dropped` del siguiente lote, para que la central sepa cuánto NO vio.
|
|
8
|
+
*/
|
|
9
|
+
export declare class LogBuffer {
|
|
10
|
+
private readonly capacidad;
|
|
11
|
+
private readonly ahora;
|
|
12
|
+
private slots;
|
|
13
|
+
private perdidas;
|
|
14
|
+
constructor(capacidad?: number, ahora?: () => number);
|
|
15
|
+
add(entrada: WireEntry, grave: boolean): boolean;
|
|
16
|
+
/** Saca hasta `max` líneas, las más viejas primero. */
|
|
17
|
+
take(max: number): WireEntry[];
|
|
18
|
+
size(): number;
|
|
19
|
+
clear(): void;
|
|
20
|
+
/** Quita lo que lleva esperando desde antes de `limite` (época en ms) y lo cuenta como perdido. Devuelve cuántas. */
|
|
21
|
+
dropOlderThan(limite: number): number;
|
|
22
|
+
addLost(n: number): void;
|
|
23
|
+
/** Lo perdido desde la última vez, y a cero. */
|
|
24
|
+
takeLost(): number;
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogBuffer = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Las líneas capturadas que esperan al siguiente envío, en memoria y con tope.
|
|
6
|
+
*
|
|
7
|
+
* Lo que llena el tope no tumba nada: una línea leve que llega con la cola llena se descarta y se cuenta como perdida; una grave
|
|
8
|
+
* (`warning` o peor) hace sitio quitando la leve más vieja, o la más vieja si todas son graves. Lo perdido viaja en el campo
|
|
9
|
+
* `dropped` del siguiente lote, para que la central sepa cuánto NO vio.
|
|
10
|
+
*/
|
|
11
|
+
class LogBuffer {
|
|
12
|
+
constructor(capacidad = 10_000, ahora = Date.now) {
|
|
13
|
+
this.capacidad = capacidad;
|
|
14
|
+
this.ahora = ahora;
|
|
15
|
+
this.slots = [];
|
|
16
|
+
this.perdidas = 0;
|
|
17
|
+
}
|
|
18
|
+
add(entrada, grave) {
|
|
19
|
+
if (this.slots.length >= this.capacidad) {
|
|
20
|
+
if (!grave) {
|
|
21
|
+
this.perdidas++;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const i = this.slots.findIndex((s) => !s.severe);
|
|
25
|
+
this.slots.splice(i === -1 ? 0 : i, 1);
|
|
26
|
+
this.perdidas++;
|
|
27
|
+
}
|
|
28
|
+
this.slots.push({ entry: entrada, severe: grave, addedAt: this.ahora() });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** Saca hasta `max` líneas, las más viejas primero. */
|
|
32
|
+
take(max) {
|
|
33
|
+
return this.slots.splice(0, max).map((s) => s.entry);
|
|
34
|
+
}
|
|
35
|
+
size() {
|
|
36
|
+
return this.slots.length;
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
this.slots = [];
|
|
40
|
+
}
|
|
41
|
+
/** Quita lo que lleva esperando desde antes de `limite` (época en ms) y lo cuenta como perdido. Devuelve cuántas. */
|
|
42
|
+
dropOlderThan(limite) {
|
|
43
|
+
const antes = this.slots.length;
|
|
44
|
+
this.slots = this.slots.filter((s) => s.addedAt >= limite);
|
|
45
|
+
this.perdidas += antes - this.slots.length;
|
|
46
|
+
return antes - this.slots.length;
|
|
47
|
+
}
|
|
48
|
+
addLost(n) {
|
|
49
|
+
this.perdidas += Math.max(0, n);
|
|
50
|
+
}
|
|
51
|
+
/** Lo perdido desde la última vez, y a cero. */
|
|
52
|
+
takeLost() {
|
|
53
|
+
const n = this.perdidas;
|
|
54
|
+
this.perdidas = 0;
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.LogBuffer = LogBuffer;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Transport } from './sender';
|
|
2
|
+
import type { LogsHost } from './types';
|
|
3
|
+
export interface DoctorResult {
|
|
4
|
+
exitCode: number;
|
|
5
|
+
lines: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorOptions {
|
|
8
|
+
/** Manda de verdad una línea de prueba (`info`) y comprueba que la central la acepta. Escribe en los logs del proyecto. */
|
|
9
|
+
send?: boolean;
|
|
10
|
+
transport?: Transport;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Comprueba de punta a punta lo que, al fallar, NO da error: sin dirección o sin clave, con la clave sin el alcance «Logs» o con el módulo
|
|
14
|
+
* apagado en el proyecto la librería no manda nada y el explorador de la mesa de ayuda queda vacío sin decir por qué. Dice qué falta y cómo
|
|
15
|
+
* arreglarlo; sale con 1 si algo impide que las líneas lleguen.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runLogsDoctor(host: LogsHost, opciones?: DoctorOptions): Promise<DoctorResult>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runLogsDoctor = runLogsDoctor;
|
|
4
|
+
const monitor_1 = require("./monitor");
|
|
5
|
+
const runtime_1 = require("./runtime");
|
|
6
|
+
const sender_1 = require("./sender");
|
|
7
|
+
const settings_1 = require("./settings");
|
|
8
|
+
/**
|
|
9
|
+
* Comprueba de punta a punta lo que, al fallar, NO da error: sin dirección o sin clave, con la clave sin el alcance «Logs» o con el módulo
|
|
10
|
+
* apagado en el proyecto la librería no manda nada y el explorador de la mesa de ayuda queda vacío sin decir por qué. Dice qué falta y cómo
|
|
11
|
+
* arreglarlo; sale con 1 si algo impide que las líneas lleguen.
|
|
12
|
+
*/
|
|
13
|
+
async function runLogsDoctor(host, opciones = {}) {
|
|
14
|
+
const lineas = [];
|
|
15
|
+
let fallos = 0;
|
|
16
|
+
const ok = (texto) => void lineas.push(`✓ ${texto}`);
|
|
17
|
+
const aviso = (texto) => void lineas.push(`⚠ ${texto}`);
|
|
18
|
+
const mal = (texto) => {
|
|
19
|
+
fallos++;
|
|
20
|
+
lineas.push(`✗ ${texto}`);
|
|
21
|
+
};
|
|
22
|
+
const env = host.env ? host.env() : ((0, runtime_1.proc)()?.env ?? {});
|
|
23
|
+
const ajustes = (0, settings_1.readLogSettings)(env);
|
|
24
|
+
const necesitaNode18 = typeof (host.fetchImpl ?? globalThis.fetch) !== 'function' || typeof CompressionStream !== 'function';
|
|
25
|
+
if (necesitaNode18) {
|
|
26
|
+
mal('Los logs necesitan Node 18.18 o superior: este no tiene `fetch` o `CompressionStream`.');
|
|
27
|
+
return { exitCode: 1, lines: lineas };
|
|
28
|
+
}
|
|
29
|
+
if (ajustes.enabled)
|
|
30
|
+
ok('Los logs están encendidos en este producto (GROWTICKETS_LOGS).');
|
|
31
|
+
else
|
|
32
|
+
aviso('GROWTICKETS_LOGS está apagado: la librería no manda ninguna línea.');
|
|
33
|
+
const destino = host.target();
|
|
34
|
+
if (!destino) {
|
|
35
|
+
mal(host.problem?.() ?? 'Faltan GROWTICKETS_URL y GROWTICKETS_API_KEY en el entorno.');
|
|
36
|
+
return { exitCode: 1, lines: lineas };
|
|
37
|
+
}
|
|
38
|
+
ok(`Hablará con ${destino.baseUrl}.`);
|
|
39
|
+
const transporte = opciones.transport ?? (0, sender_1.createTransport)(host);
|
|
40
|
+
const respuesta = await transporte.get('config');
|
|
41
|
+
const datos = respuesta?.json?.data;
|
|
42
|
+
if (respuesta === null)
|
|
43
|
+
mal('La mesa de ayuda no contestó: comprueba la dirección, la red y que el cortafuegos deje salir esta conexión.');
|
|
44
|
+
else if (respuesta.status === 401)
|
|
45
|
+
mal('La mesa de ayuda no reconoce la clave de API (401): revisa GROWTICKETS_API_KEY.');
|
|
46
|
+
else if (respuesta.status === 403)
|
|
47
|
+
mal('Esta clave no tiene acceso a Logs (403): márcalo en «Claves de API» del proyecto.');
|
|
48
|
+
else if (respuesta.status === 404)
|
|
49
|
+
mal('Esa dirección no tiene la ingesta de logs (404): ¿es la de GrowTickets, y una versión con logs?');
|
|
50
|
+
else if (respuesta.status < 200 || respuesta.status >= 300)
|
|
51
|
+
mal(`La mesa de ayuda contestó con un error (HTTP ${respuesta.status}).`);
|
|
52
|
+
else if (datos?.enabled === false)
|
|
53
|
+
mal('El módulo «Logs» está apagado en este proyecto: enciéndelo en su pestaña «General».');
|
|
54
|
+
else
|
|
55
|
+
ok('La mesa de ayuda acepta los logs de este proyecto.');
|
|
56
|
+
if (opciones.send && fallos === 0) {
|
|
57
|
+
const monitor = new monitor_1.LogMonitor(host, { transport: transporte });
|
|
58
|
+
monitor.capture({ level: 'info', message: 'growtickets doctor: línea de prueba', channel: 'doctor' });
|
|
59
|
+
const envio = await monitor.flush(true);
|
|
60
|
+
if (envio.status === 'sent')
|
|
61
|
+
ok('Una línea de prueba llegó: búscala en el explorador de logs del proyecto.');
|
|
62
|
+
else
|
|
63
|
+
mal(`La línea de prueba no llegó (${envio.status}).`);
|
|
64
|
+
}
|
|
65
|
+
return { exitCode: fallos > 0 ? 1 : 0, lines: lineas };
|
|
66
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Redactor } from './redactor';
|
|
2
|
+
import type { LogLimits, LogRecord, WireEntry } from './types';
|
|
3
|
+
export declare const DEFAULT_LIMITS: LogLimits;
|
|
4
|
+
export interface BuildOptions {
|
|
5
|
+
limits: LogLimits;
|
|
6
|
+
redactor: Redactor;
|
|
7
|
+
/** La carpeta del producto: las rutas de la traza van relativas a ella (más cortas, estables entre despliegues). */
|
|
8
|
+
root: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Convierte lo que entrega un adaptador en la línea del protocolo: con los secretos tapados, el contexto sin objetos vivos y todo
|
|
12
|
+
* acotado a lo que la central dijo admitir (`logs/config`). Nunca lanza hacia fuera: si una parte no se puede convertir, esa parte
|
|
13
|
+
* se omite y la línea sale igual.
|
|
14
|
+
*
|
|
15
|
+
* ## La traza no lleva argumentos ni valores
|
|
16
|
+
*
|
|
17
|
+
* Se arma de las líneas `at …` del `stack`, que sólo traen función, archivo y línea; el mensaje del error va aparte y también pasa por el
|
|
18
|
+
* tapado. Las rutas son relativas a la carpeta del producto.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildEntry(registro: LogRecord, opciones: BuildOptions): WireEntry;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_LIMITS = void 0;
|
|
4
|
+
exports.buildEntry = buildEntry;
|
|
5
|
+
const normalize_1 = require("./normalize");
|
|
6
|
+
const runtime_1 = require("./runtime");
|
|
7
|
+
/** Lo más que ocupa la traza de una línea, en bytes: la central corta en 12.288. */
|
|
8
|
+
const TRACE_BYTES = 12_288;
|
|
9
|
+
const MAX_CAUSES = 3;
|
|
10
|
+
const CAUSE_FRAMES = 5;
|
|
11
|
+
exports.DEFAULT_LIMITS = { messageBytes: 8192, contextBytes: 16_384, traceFrames: 50 };
|
|
12
|
+
/**
|
|
13
|
+
* Convierte lo que entrega un adaptador en la línea del protocolo: con los secretos tapados, el contexto sin objetos vivos y todo
|
|
14
|
+
* acotado a lo que la central dijo admitir (`logs/config`). Nunca lanza hacia fuera: si una parte no se puede convertir, esa parte
|
|
15
|
+
* se omite y la línea sale igual.
|
|
16
|
+
*
|
|
17
|
+
* ## La traza no lleva argumentos ni valores
|
|
18
|
+
*
|
|
19
|
+
* Se arma de las líneas `at …` del `stack`, que sólo traen función, archivo y línea; el mensaje del error va aparte y también pasa por el
|
|
20
|
+
* tapado. Las rutas son relativas a la carpeta del producto.
|
|
21
|
+
*/
|
|
22
|
+
function buildEntry(registro, opciones) {
|
|
23
|
+
const { limits, redactor } = opciones;
|
|
24
|
+
const entrada = {
|
|
25
|
+
at: toIso(registro.time),
|
|
26
|
+
level: registro.level,
|
|
27
|
+
message: redactAndCut(redactor, String(registro.message ?? '').trim(), limits.messageBytes),
|
|
28
|
+
};
|
|
29
|
+
const canal = typeof registro.channel === 'string' ? registro.channel.trim().slice(0, 60) : '';
|
|
30
|
+
if (canal !== '')
|
|
31
|
+
entrada.channel = canal;
|
|
32
|
+
const contexto = contextOf(registro.context, limits.contextBytes, redactor);
|
|
33
|
+
if (contexto)
|
|
34
|
+
entrada.context = contexto;
|
|
35
|
+
const excepcion = registro.error ? exceptionOf(registro.error, limits.traceFrames, opciones) : null;
|
|
36
|
+
if (excepcion)
|
|
37
|
+
entrada.exception = excepcion;
|
|
38
|
+
return entrada;
|
|
39
|
+
}
|
|
40
|
+
function toIso(tiempo) {
|
|
41
|
+
let fecha;
|
|
42
|
+
if (typeof tiempo === 'number')
|
|
43
|
+
fecha = new Date(tiempo > 1e11 ? tiempo : tiempo * 1000);
|
|
44
|
+
else if (tiempo instanceof Date)
|
|
45
|
+
fecha = tiempo;
|
|
46
|
+
else if (typeof tiempo === 'string' && tiempo !== '')
|
|
47
|
+
fecha = new Date(tiempo);
|
|
48
|
+
else
|
|
49
|
+
fecha = new Date();
|
|
50
|
+
return (Number.isNaN(fecha.getTime()) ? new Date() : fecha).toISOString();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* El contexto, ya convertido, tapado y acotado en bytes. Si no cabe se queda con las primeras claves que quepan y lo dice con
|
|
54
|
+
* `_truncated`: mejor un contexto incompleto que perder la línea.
|
|
55
|
+
*/
|
|
56
|
+
function contextOf(contexto, bytes, redactor) {
|
|
57
|
+
const seguro = (0, normalize_1.safeContext)(contexto);
|
|
58
|
+
if (!seguro)
|
|
59
|
+
return null;
|
|
60
|
+
const limpio = redactor.context(seguro);
|
|
61
|
+
if ((0, runtime_1.byteLength)(JSON.stringify(limpio)) <= bytes)
|
|
62
|
+
return limpio;
|
|
63
|
+
const cabe = {};
|
|
64
|
+
for (const [clave, valor] of Object.entries(limpio)) {
|
|
65
|
+
if ((0, runtime_1.byteLength)(JSON.stringify({ ...cabe, [clave]: valor, _truncated: true })) > bytes)
|
|
66
|
+
break;
|
|
67
|
+
cabe[clave] = valor;
|
|
68
|
+
}
|
|
69
|
+
return { ...cabe, _truncated: true };
|
|
70
|
+
}
|
|
71
|
+
function exceptionOf(error, marcos, opciones) {
|
|
72
|
+
const clase = text(error.type) ?? text(error.name) ?? 'Error';
|
|
73
|
+
const mensaje = text(error.message) ?? '';
|
|
74
|
+
const pila = text(error.stack);
|
|
75
|
+
const primero = pila ? firstFrame(pila) : null;
|
|
76
|
+
let traza = pila ? traceOf(pila, marcos, opciones.root) : `${clase}${mensaje ? `: ${mensaje}` : ''}`;
|
|
77
|
+
for (let causa = error.cause, i = 0; causa && typeof causa === 'object' && i < MAX_CAUSES; causa = causa.cause, i++) {
|
|
78
|
+
const c = causa;
|
|
79
|
+
const nombre = text(c.type) ?? text(c.name) ?? 'Error';
|
|
80
|
+
const cuerpo = text(c.stack) ? traceOf(text(c.stack), CAUSE_FRAMES, opciones.root) : `${nombre}: ${text(c.message) ?? ''}`;
|
|
81
|
+
traza += `\n\nCaused by: ${cuerpo}`;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
class: (0, normalize_1.cutString)(clase, 191),
|
|
85
|
+
file: primero ? (0, normalize_1.cutString)(relative(primero.file, opciones.root), 255) : null,
|
|
86
|
+
line: primero ? primero.line : null,
|
|
87
|
+
trace: redactAndCut(opciones.redactor, traza, TRACE_BYTES),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function text(valor) {
|
|
91
|
+
return typeof valor === 'string' && valor !== '' ? valor : null;
|
|
92
|
+
}
|
|
93
|
+
/** El encabezado del `stack` (clase y mensaje, que puede ocupar varias líneas) y sus primeras `marcos` líneas `at …`, con rutas relativas. */
|
|
94
|
+
function traceOf(pila, marcos, root) {
|
|
95
|
+
const lineas = pila.split('\n');
|
|
96
|
+
const inicio = lineas.findIndex((linea) => /^\s+at /.test(linea));
|
|
97
|
+
const encabezado = inicio === -1 ? lineas : lineas.slice(0, inicio);
|
|
98
|
+
const frames = inicio === -1 ? [] : lineas.slice(inicio).filter((linea) => /^\s+at /.test(linea));
|
|
99
|
+
return [...encabezado, ...frames.slice(0, Math.max(1, marcos))].map((linea) => relative(linea, root)).join('\n');
|
|
100
|
+
}
|
|
101
|
+
const FRAME = /^\s+at (?:.+? \()?(.+?):(\d+):\d+\)?$/;
|
|
102
|
+
/** El primer marco que no es interno de Node: de ahí salen el archivo y la línea, como los de la excepción de Laravel. */
|
|
103
|
+
function firstFrame(pila) {
|
|
104
|
+
for (const linea of pila.split('\n')) {
|
|
105
|
+
const marco = FRAME.exec(linea);
|
|
106
|
+
if (!marco || !marco[1] || !marco[2])
|
|
107
|
+
continue;
|
|
108
|
+
const archivo = marco[1].replace(/^file:\/\//, '');
|
|
109
|
+
if (archivo.startsWith('node:') || archivo === '<anonymous>')
|
|
110
|
+
continue;
|
|
111
|
+
return { file: archivo, line: Number(marco[2]) };
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function relative(ruta, root) {
|
|
116
|
+
if (root.length < 2)
|
|
117
|
+
return ruta;
|
|
118
|
+
const base = root.endsWith('/') ? root : `${root}/`;
|
|
119
|
+
return ruta.split(`file://${base}`).join('').split(base).join('');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Tapar ANTES de cortar: cortando primero, un secreto partido justo en el borde ya no casaba ni como literal ni con su patrón y salía
|
|
123
|
+
* la mitad. Se tapa una ventana del doble (no el texto entero, que puede ser enorme) y después se corta.
|
|
124
|
+
*/
|
|
125
|
+
function redactAndCut(redactor, texto, bytes) {
|
|
126
|
+
return (0, runtime_1.cutBytes)(redactor.text((0, runtime_1.cutBytes)(texto, bytes * 2)), bytes);
|
|
127
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { LogBuffer } from './buffer';
|
|
2
|
+
import type { RemoteConfig } from './remote-config';
|
|
3
|
+
import { type Transport } from './sender';
|
|
4
|
+
import type { LogSettings } from './settings';
|
|
5
|
+
import type { LogsHost } from './types';
|
|
6
|
+
/** Lo que lleva más de esto sin enviarse (por una caída de la mesa de ayuda) se descarta y se cuenta. */
|
|
7
|
+
export declare const MAX_AGE_MS = 3600000;
|
|
8
|
+
export type FlushStatus = 'idle' | 'disabled' | 'backoff' | 'sent' | 'failed' | 'busy' | 'unconfigured';
|
|
9
|
+
export interface FlushResult {
|
|
10
|
+
status: FlushStatus;
|
|
11
|
+
entries: number;
|
|
12
|
+
chunks: number;
|
|
13
|
+
dropped: number;
|
|
14
|
+
rejected: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FlusherDeps {
|
|
17
|
+
host: LogsHost;
|
|
18
|
+
transport: Transport;
|
|
19
|
+
remote: RemoteConfig;
|
|
20
|
+
buffer: LogBuffer;
|
|
21
|
+
settings: () => LogSettings;
|
|
22
|
+
warn: (clave: string, mensaje: string) => void;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
uuid?: () => string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Saca lo capturado de la cola y lo manda a la mesa de ayuda en lotes comprimidos.
|
|
28
|
+
*
|
|
29
|
+
* ## Un lote se REINTENTA con el mismo identificador
|
|
30
|
+
*
|
|
31
|
+
* Si el envío expira no se sabe si la central lo procesó. Reenviar el MISMO lote con el mismo `batch_id` es seguro (la central reconoce
|
|
32
|
+
* el duplicado); reenviar otro conjunto duplicaría las líneas. Por eso un lote en vuelo se guarda (`pending`) y es lo primero que se
|
|
33
|
+
* reintenta.
|
|
34
|
+
*
|
|
35
|
+
* ## Qué pasa cuando falla
|
|
36
|
+
*
|
|
37
|
+
* Sin respuesta, 401, 403, 408, 429 o 5xx: se conserva y se reintenta; tras tres fallos seguidos hay un corte de cinco minutos. Otro 4xx
|
|
38
|
+
* (el lote es inválido) no lo arregla ningún reintento: se descarta y se cuenta como perdido. Lo que lleva más de una hora sin enviarse
|
|
39
|
+
* se descarta también. Todo lo perdido viaja en `dropped` del siguiente lote: la central sabe cuánto NO vio. Si responde que los logs
|
|
40
|
+
* están apagados, se descarta todo y se deja de capturar hasta que vuelva a decir que sí.
|
|
41
|
+
*
|
|
42
|
+
* Sólo hay UN envío en vuelo a la vez, y nada de esto lanza hacia fuera.
|
|
43
|
+
*/
|
|
44
|
+
export declare class Flusher {
|
|
45
|
+
private readonly deps;
|
|
46
|
+
private pendientes;
|
|
47
|
+
private fallos;
|
|
48
|
+
private reintentoEn;
|
|
49
|
+
private enVuelo;
|
|
50
|
+
private readonly ahora;
|
|
51
|
+
private readonly uuid;
|
|
52
|
+
constructor(deps: FlusherDeps);
|
|
53
|
+
flush(opciones?: {
|
|
54
|
+
force?: boolean;
|
|
55
|
+
final?: boolean;
|
|
56
|
+
}): Promise<FlushResult>;
|
|
57
|
+
/** Lo que hay sin confirmar y cómo van los reintentos. Lo usan las pruebas y `doctor`. */
|
|
58
|
+
state(): {
|
|
59
|
+
pendientes: number;
|
|
60
|
+
fallos: number;
|
|
61
|
+
reintentoEn: number;
|
|
62
|
+
};
|
|
63
|
+
private enviarTodo;
|
|
64
|
+
private descartarTodo;
|
|
65
|
+
private descartarViejo;
|
|
66
|
+
/** Parte lo que hay en la cola en lotes de `max_entries` líneas y de poco más de tres megas, el primero con lo perdido hasta ahora. */
|
|
67
|
+
private formarLotes;
|
|
68
|
+
/** Un lote que comprimido no cabe se parte por la mitad: cada mitad con su identificador, y lo perdido en la primera. */
|
|
69
|
+
private partir;
|
|
70
|
+
private cuerpo;
|
|
71
|
+
private fallar;
|
|
72
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Flusher = exports.MAX_AGE_MS = void 0;
|
|
4
|
+
const runtime_1 = require("./runtime");
|
|
5
|
+
const sender_1 = require("./sender");
|
|
6
|
+
const FAILURES_BEFORE_BREAK = 3;
|
|
7
|
+
const BREAK_MS = 300_000;
|
|
8
|
+
const MAX_CHUNKS = 10;
|
|
9
|
+
const FINAL_CHUNKS = 20;
|
|
10
|
+
/** Lo más que pesa un lote sin comprimir (la central corta en 4 MiB) y lo más que pesa comprimido (corta en 1 MiB). */
|
|
11
|
+
const MAX_BODY_CHARS = 3_000_000;
|
|
12
|
+
const MAX_GZIP_BYTES = 1_000_000;
|
|
13
|
+
/** Lo que lleva más de esto sin enviarse (por una caída de la mesa de ayuda) se descarta y se cuenta. */
|
|
14
|
+
exports.MAX_AGE_MS = 3_600_000;
|
|
15
|
+
/**
|
|
16
|
+
* Saca lo capturado de la cola y lo manda a la mesa de ayuda en lotes comprimidos.
|
|
17
|
+
*
|
|
18
|
+
* ## Un lote se REINTENTA con el mismo identificador
|
|
19
|
+
*
|
|
20
|
+
* Si el envío expira no se sabe si la central lo procesó. Reenviar el MISMO lote con el mismo `batch_id` es seguro (la central reconoce
|
|
21
|
+
* el duplicado); reenviar otro conjunto duplicaría las líneas. Por eso un lote en vuelo se guarda (`pending`) y es lo primero que se
|
|
22
|
+
* reintenta.
|
|
23
|
+
*
|
|
24
|
+
* ## Qué pasa cuando falla
|
|
25
|
+
*
|
|
26
|
+
* Sin respuesta, 401, 403, 408, 429 o 5xx: se conserva y se reintenta; tras tres fallos seguidos hay un corte de cinco minutos. Otro 4xx
|
|
27
|
+
* (el lote es inválido) no lo arregla ningún reintento: se descarta y se cuenta como perdido. Lo que lleva más de una hora sin enviarse
|
|
28
|
+
* se descarta también. Todo lo perdido viaja en `dropped` del siguiente lote: la central sabe cuánto NO vio. Si responde que los logs
|
|
29
|
+
* están apagados, se descarta todo y se deja de capturar hasta que vuelva a decir que sí.
|
|
30
|
+
*
|
|
31
|
+
* Sólo hay UN envío en vuelo a la vez, y nada de esto lanza hacia fuera.
|
|
32
|
+
*/
|
|
33
|
+
class Flusher {
|
|
34
|
+
constructor(deps) {
|
|
35
|
+
this.deps = deps;
|
|
36
|
+
this.pendientes = [];
|
|
37
|
+
this.fallos = 0;
|
|
38
|
+
this.reintentoEn = 0;
|
|
39
|
+
this.enVuelo = false;
|
|
40
|
+
this.ahora = deps.now ?? Date.now;
|
|
41
|
+
this.uuid = deps.uuid ?? runtime_1.uuid;
|
|
42
|
+
}
|
|
43
|
+
async flush(opciones = {}) {
|
|
44
|
+
if (this.enVuelo)
|
|
45
|
+
return resultado('busy');
|
|
46
|
+
this.enVuelo = true;
|
|
47
|
+
try {
|
|
48
|
+
return await this.enviarTodo(opciones.force === true, opciones.final === true);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return resultado('failed');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
this.enVuelo = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Lo que hay sin confirmar y cómo van los reintentos. Lo usan las pruebas y `doctor`. */
|
|
58
|
+
state() {
|
|
59
|
+
return { pendientes: this.pendientes.reduce((n, c) => n + c.entries.length, 0), fallos: this.fallos, reintentoEn: this.reintentoEn };
|
|
60
|
+
}
|
|
61
|
+
async enviarTodo(forzar, final) {
|
|
62
|
+
const { host, remote, buffer, transport } = this.deps;
|
|
63
|
+
const salida = resultado('idle');
|
|
64
|
+
if (!this.deps.settings().enabled) {
|
|
65
|
+
this.descartarTodo();
|
|
66
|
+
return salida;
|
|
67
|
+
}
|
|
68
|
+
if (!host.target())
|
|
69
|
+
return { ...salida, status: 'unconfigured' };
|
|
70
|
+
await remote.refreshIfStale();
|
|
71
|
+
if (remote.enabled() === false) {
|
|
72
|
+
this.descartarTodo();
|
|
73
|
+
return { ...salida, status: 'disabled' };
|
|
74
|
+
}
|
|
75
|
+
const ahora = this.ahora();
|
|
76
|
+
if (!forzar && ahora < this.reintentoEn)
|
|
77
|
+
return { ...salida, status: 'backoff' };
|
|
78
|
+
this.descartarViejo(ahora);
|
|
79
|
+
if (this.pendientes.length === 0)
|
|
80
|
+
this.formarLotes(final ? FINAL_CHUNKS : MAX_CHUNKS, ahora);
|
|
81
|
+
if (this.pendientes.length === 0)
|
|
82
|
+
return salida;
|
|
83
|
+
while (this.pendientes.length > 0) {
|
|
84
|
+
const lote = this.pendientes[0];
|
|
85
|
+
const comprimido = await (0, sender_1.gzipJson)(this.cuerpo(lote, ahora));
|
|
86
|
+
if (comprimido.length > MAX_GZIP_BYTES && lote.entries.length > 1) {
|
|
87
|
+
this.partir(lote);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const respuesta = await transport.post('batches', comprimido);
|
|
91
|
+
if (respuesta === null || reintentable(respuesta.status)) {
|
|
92
|
+
this.fallar(respuesta?.status ?? null, ahora);
|
|
93
|
+
return { ...salida, status: 'failed' };
|
|
94
|
+
}
|
|
95
|
+
this.pendientes.shift();
|
|
96
|
+
if (respuesta.status < 200 || respuesta.status >= 300) {
|
|
97
|
+
buffer.addLost(lote.entries.length + lote.dropped);
|
|
98
|
+
this.deps.warn('rechazado', `La mesa de ayuda rechazó un lote de logs (HTTP ${respuesta.status}) y se descarta.`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const datos = respuesta.json?.data;
|
|
102
|
+
if (datos?.enabled === false) {
|
|
103
|
+
remote.markDisabled();
|
|
104
|
+
this.descartarTodo();
|
|
105
|
+
return { ...salida, status: 'disabled' };
|
|
106
|
+
}
|
|
107
|
+
salida.entries += lote.entries.length;
|
|
108
|
+
salida.chunks++;
|
|
109
|
+
salida.dropped += lote.dropped;
|
|
110
|
+
salida.rejected += Array.isArray(datos?.rejected) ? datos.rejected.length : 0;
|
|
111
|
+
}
|
|
112
|
+
this.fallos = 0;
|
|
113
|
+
this.reintentoEn = 0;
|
|
114
|
+
return { ...salida, status: salida.chunks > 0 ? 'sent' : 'idle' };
|
|
115
|
+
}
|
|
116
|
+
descartarTodo() {
|
|
117
|
+
this.deps.buffer.clear();
|
|
118
|
+
this.deps.buffer.takeLost();
|
|
119
|
+
this.pendientes = [];
|
|
120
|
+
}
|
|
121
|
+
descartarViejo(ahora) {
|
|
122
|
+
const limite = ahora - exports.MAX_AGE_MS;
|
|
123
|
+
this.deps.buffer.dropOlderThan(limite);
|
|
124
|
+
const vigentes = this.pendientes.filter((c) => c.formedAt >= limite);
|
|
125
|
+
for (const caduco of this.pendientes.filter((c) => c.formedAt < limite))
|
|
126
|
+
this.deps.buffer.addLost(caduco.entries.length + caduco.dropped);
|
|
127
|
+
this.pendientes = vigentes;
|
|
128
|
+
}
|
|
129
|
+
/** Parte lo que hay en la cola en lotes de `max_entries` líneas y de poco más de tres megas, el primero con lo perdido hasta ahora. */
|
|
130
|
+
formarLotes(max, ahora) {
|
|
131
|
+
const { buffer, remote } = this.deps;
|
|
132
|
+
const entradas = buffer.take(remote.maxEntries() * max);
|
|
133
|
+
if (entradas.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
const lotes = [];
|
|
136
|
+
let actual = [];
|
|
137
|
+
let peso = 0;
|
|
138
|
+
const cerrar = () => {
|
|
139
|
+
if (actual.length > 0)
|
|
140
|
+
lotes.push({ id: this.uuid(), entries: actual, dropped: 0, formedAt: ahora });
|
|
141
|
+
actual = [];
|
|
142
|
+
peso = 0;
|
|
143
|
+
};
|
|
144
|
+
for (const entrada of entradas) {
|
|
145
|
+
const largo = JSON.stringify(entrada).length;
|
|
146
|
+
if (actual.length >= remote.maxEntries() || (peso + largo > MAX_BODY_CHARS && actual.length > 0))
|
|
147
|
+
cerrar();
|
|
148
|
+
actual.push(entrada);
|
|
149
|
+
peso += largo;
|
|
150
|
+
}
|
|
151
|
+
cerrar();
|
|
152
|
+
lotes[0].dropped = buffer.takeLost();
|
|
153
|
+
this.pendientes = lotes;
|
|
154
|
+
}
|
|
155
|
+
/** Un lote que comprimido no cabe se parte por la mitad: cada mitad con su identificador, y lo perdido en la primera. */
|
|
156
|
+
partir(lote) {
|
|
157
|
+
const mitad = Math.ceil(lote.entries.length / 2);
|
|
158
|
+
this.pendientes.splice(0, 1, { id: this.uuid(), entries: lote.entries.slice(0, mitad), dropped: lote.dropped, formedAt: lote.formedAt }, { id: this.uuid(), entries: lote.entries.slice(mitad), dropped: 0, formedAt: lote.formedAt });
|
|
159
|
+
}
|
|
160
|
+
cuerpo(lote, ahora) {
|
|
161
|
+
const origen = this.deps.settings().origin;
|
|
162
|
+
return {
|
|
163
|
+
protocol: 1,
|
|
164
|
+
batch_id: lote.id,
|
|
165
|
+
sent_at: new Date(ahora).toISOString(),
|
|
166
|
+
dropped: lote.dropped,
|
|
167
|
+
sdk: this.deps.host.sdk,
|
|
168
|
+
instance: this.deps.host.instance,
|
|
169
|
+
remote: 'off',
|
|
170
|
+
entries: lote.entries,
|
|
171
|
+
...(origen ? { origin: origen } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
fallar(estado, ahora) {
|
|
175
|
+
this.fallos++;
|
|
176
|
+
if (estado === 401 || estado === 403) {
|
|
177
|
+
this.deps.warn('clave', `La mesa de ayuda rechazó la clave de API (HTTP ${estado}): comprueba GROWTICKETS_API_KEY y que lleve el alcance «Logs».`);
|
|
178
|
+
}
|
|
179
|
+
if (this.fallos >= FAILURES_BEFORE_BREAK)
|
|
180
|
+
this.reintentoEn = ahora + BREAK_MS;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.Flusher = Flusher;
|
|
184
|
+
function reintentable(estado) {
|
|
185
|
+
return estado === 401 || estado === 403 || estado === 408 || estado === 429 || estado >= 500;
|
|
186
|
+
}
|
|
187
|
+
function resultado(status) {
|
|
188
|
+
return { status, entries: 0, chunks: 0, dropped: 0, rejected: 0 };
|
|
189
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Lo que `console.log` haría con los argumentos: la sustitución estilo printf del primero (`%s`, `%d`, `%j`…) y el resto unido con espacios. */
|
|
2
|
+
export declare function formatArgs(args: readonly unknown[]): string;
|
|
3
|
+
/** Un valor como texto, sin ejecutar nada suyo: un `Error` como `Clase: mensaje`, un objeto como JSON (acotado y sin ciclos). */
|
|
4
|
+
export declare function stringify(valor: unknown): string;
|