autotel 2.25.5 → 2.26.1
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 +47 -1
- package/dist/attribute-redacting-processor.cjs +14 -6
- package/dist/attribute-redacting-processor.d.cts +63 -1
- package/dist/attribute-redacting-processor.d.ts +63 -1
- package/dist/attribute-redacting-processor.js +1 -1
- package/dist/attributes.cjs +21 -21
- package/dist/attributes.js +2 -2
- package/dist/auto.cjs +8 -8
- package/dist/auto.js +6 -6
- package/dist/{chunk-RUD7KS4R.js → chunk-3SDILILG.js} +3 -3
- package/dist/{chunk-RUD7KS4R.js.map → chunk-3SDILILG.js.map} +1 -1
- package/dist/{chunk-B33XPEKY.js → chunk-55ER2KD5.js} +4 -4
- package/dist/chunk-55ER2KD5.js.map +1 -0
- package/dist/{chunk-ITYASFHQ.cjs → chunk-563EL6O6.cjs} +153 -15
- package/dist/chunk-563EL6O6.cjs.map +1 -0
- package/dist/{chunk-EEJGUBWV.cjs → chunk-6YGUN7IY.cjs} +5 -5
- package/dist/{chunk-EEJGUBWV.cjs.map → chunk-6YGUN7IY.cjs.map} +1 -1
- package/dist/{chunk-XDKK53OL.js → chunk-A4E5AQFK.js} +3 -3
- package/dist/{chunk-XDKK53OL.js.map → chunk-A4E5AQFK.js.map} +1 -1
- package/dist/{chunk-XB2GITM5.js → chunk-BJ2XPN77.js} +3 -3
- package/dist/{chunk-XB2GITM5.js.map → chunk-BJ2XPN77.js.map} +1 -1
- package/dist/{chunk-KZEC4CHV.cjs → chunk-CEAQK2QY.cjs} +5 -5
- package/dist/{chunk-KZEC4CHV.cjs.map → chunk-CEAQK2QY.cjs.map} +1 -1
- package/dist/chunk-CMNGGTQL.cjs +349 -0
- package/dist/chunk-CMNGGTQL.cjs.map +1 -0
- package/dist/{chunk-VYA6QDNA.js → chunk-DPSA4QLA.js} +4 -2
- package/dist/chunk-DPSA4QLA.js.map +1 -0
- package/dist/{chunk-M4US3P4K.js → chunk-ER43K7ES.js} +3 -3
- package/dist/{chunk-M4US3P4K.js.map → chunk-ER43K7ES.js.map} +1 -1
- package/dist/{chunk-AZ24DJAG.cjs → chunk-FU6R566Y.cjs} +4 -4
- package/dist/chunk-FU6R566Y.cjs.map +1 -0
- package/dist/{chunk-OPTGXEVN.js → chunk-HPUGKUMZ.js} +4 -4
- package/dist/{chunk-OPTGXEVN.js.map → chunk-HPUGKUMZ.js.map} +1 -1
- package/dist/{chunk-XRBP4RYL.cjs → chunk-JKIMEPI2.cjs} +4 -4
- package/dist/{chunk-XRBP4RYL.cjs.map → chunk-JKIMEPI2.cjs.map} +1 -1
- package/dist/{chunk-I6JPSD4R.cjs → chunk-OBWXM4NN.cjs} +9 -9
- package/dist/{chunk-I6JPSD4R.cjs.map → chunk-OBWXM4NN.cjs.map} +1 -1
- package/dist/{chunk-OFPZULMQ.cjs → chunk-OC6X2VIN.cjs} +8 -8
- package/dist/{chunk-OFPZULMQ.cjs.map → chunk-OC6X2VIN.cjs.map} +1 -1
- package/dist/{chunk-QDREXAD7.js → chunk-QC5MNKVF.js} +4 -4
- package/dist/{chunk-QDREXAD7.js.map → chunk-QC5MNKVF.js.map} +1 -1
- package/dist/chunk-TDNKIHKT.js +341 -0
- package/dist/chunk-TDNKIHKT.js.map +1 -0
- package/dist/{chunk-CMUM4JQI.js → chunk-U54FTVFH.js} +3 -3
- package/dist/{chunk-CMUM4JQI.js.map → chunk-U54FTVFH.js.map} +1 -1
- package/dist/{chunk-QQLP4M6W.cjs → chunk-UTZR7P7E.cjs} +36 -36
- package/dist/{chunk-QQLP4M6W.cjs.map → chunk-UTZR7P7E.cjs.map} +1 -1
- package/dist/{chunk-7FIGORWI.cjs → chunk-VH77IPJN.cjs} +4 -2
- package/dist/chunk-VH77IPJN.cjs.map +1 -0
- package/dist/{chunk-MNBAXRVG.js → chunk-W35FVJBC.js} +144 -9
- package/dist/chunk-W35FVJBC.js.map +1 -0
- package/dist/{chunk-A5ZUL2RZ.cjs → chunk-WZOKY3PW.cjs} +13 -13
- package/dist/{chunk-A5ZUL2RZ.cjs.map → chunk-WZOKY3PW.cjs.map} +1 -1
- package/dist/{chunk-MN6PZ4AN.cjs → chunk-YEVCD6DR.cjs} +7 -7
- package/dist/{chunk-MN6PZ4AN.cjs.map → chunk-YEVCD6DR.cjs.map} +1 -1
- package/dist/{chunk-WYP6OOCT.js → chunk-YN7USLHW.js} +3 -3
- package/dist/{chunk-WYP6OOCT.js.map → chunk-YN7USLHW.js.map} +1 -1
- package/dist/decorators.cjs +7 -7
- package/dist/decorators.js +7 -7
- package/dist/event.cjs +10 -10
- package/dist/event.js +7 -7
- package/dist/functional.cjs +14 -14
- package/dist/functional.js +7 -7
- package/dist/index.cjs +214 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +142 -33
- package/dist/index.js.map +1 -1
- package/dist/{init-CIzpC5kZ.d.cts → init-CMuTaFAV.d.cts} +49 -1
- package/dist/{init-C_PiC_Su.d.ts → init-D6JfWEjL.d.ts} +49 -1
- package/dist/instrumentation.cjs +14 -14
- package/dist/instrumentation.js +6 -6
- package/dist/logger.cjs +8 -8
- package/dist/logger.js +1 -1
- package/dist/messaging.cjs +11 -11
- package/dist/messaging.js +8 -8
- package/dist/metric.cjs +1 -1
- package/dist/metric.js +1 -1
- package/dist/sampling.cjs +15 -15
- package/dist/sampling.js +2 -2
- package/dist/semantic-helpers.cjs +12 -12
- package/dist/semantic-helpers.js +8 -8
- package/dist/tail-sampling-processor.cjs +4 -4
- package/dist/tail-sampling-processor.js +3 -3
- package/dist/testing.cjs +1 -1
- package/dist/testing.js +1 -1
- package/dist/webhook.cjs +9 -8
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.js +8 -7
- package/dist/webhook.js.map +1 -1
- package/dist/workflow-distributed.cjs +9 -9
- package/dist/workflow-distributed.js +7 -7
- package/dist/workflow.cjs +12 -12
- package/dist/workflow.js +8 -8
- package/dist/yaml-config.cjs +6 -6
- package/dist/yaml-config.d.cts +1 -1
- package/dist/yaml-config.d.ts +1 -1
- package/dist/yaml-config.js +3 -3
- package/package.json +41 -41
- package/src/attribute-redacting-processor.test.ts +81 -16
- package/src/attribute-redacting-processor.ts +278 -24
- package/src/autotel-logger.ts +2 -2
- package/src/devtools.ts +60 -0
- package/src/hook.mjs +2 -2
- package/src/index.ts +2 -1
- package/src/init.customization.test.ts +81 -0
- package/src/init.ts +188 -3
- package/src/request-logger.test.ts +266 -1
- package/src/request-logger.ts +115 -16
- package/src/shutdown.test.ts +35 -1
- package/src/shutdown.ts +3 -1
- package/src/structured-error.ts +54 -1
- package/dist/chunk-7FIGORWI.cjs.map +0 -1
- package/dist/chunk-AZ24DJAG.cjs.map +0 -1
- package/dist/chunk-B33XPEKY.js.map +0 -1
- package/dist/chunk-ELW34S4C.cjs +0 -173
- package/dist/chunk-ELW34S4C.cjs.map +0 -1
- package/dist/chunk-ITYASFHQ.cjs.map +0 -1
- package/dist/chunk-MNBAXRVG.js.map +0 -1
- package/dist/chunk-SNINLBEE.js +0 -167
- package/dist/chunk-SNINLBEE.js.map +0 -1
- package/dist/chunk-VYA6QDNA.js.map +0 -1
package/src/init.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
} from './span-name-normalizer';
|
|
61
61
|
import {
|
|
62
62
|
AttributeRedactingProcessor,
|
|
63
|
+
normalizeAttributeRedactorConfig,
|
|
63
64
|
type AttributeRedactorConfig,
|
|
64
65
|
type AttributeRedactorPreset,
|
|
65
66
|
} from './attribute-redacting-processor';
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
type CanonicalLogLineOptions,
|
|
74
75
|
} from './processors/canonical-log-line-processor';
|
|
75
76
|
import type { EventsConfig } from './events-config';
|
|
77
|
+
import { resolveDevtoolsConfig, type AutotelDevtoolsConfig } from './devtools';
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
80
|
* Silent logger (no-op) - used as default when user doesn't provide one.
|
|
@@ -297,6 +299,21 @@ export interface AutotelConfig {
|
|
|
297
299
|
/** Service name (required) */
|
|
298
300
|
service: string;
|
|
299
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Local developer UX for autotel-devtools.
|
|
304
|
+
*
|
|
305
|
+
* - `true`: send traces, metrics, and logs to `http://127.0.0.1:4318`
|
|
306
|
+
* - `{ embedded: true }`: attempt to start `autotel-devtools` automatically
|
|
307
|
+
*
|
|
308
|
+
* When enabled:
|
|
309
|
+
* - `endpoint` defaults to the local devtools URL
|
|
310
|
+
* - `logs` default to `true` unless explicitly set
|
|
311
|
+
*
|
|
312
|
+
* This keeps production config unchanged while making local debugging
|
|
313
|
+
* effectively zero-config.
|
|
314
|
+
*/
|
|
315
|
+
devtools?: boolean | AutotelDevtoolsConfig;
|
|
316
|
+
|
|
300
317
|
/** Event subscribers - bring your own (PostHog, Mixpanel, etc.) */
|
|
301
318
|
subscribers?: EventSubscriber[];
|
|
302
319
|
|
|
@@ -1126,10 +1143,28 @@ export interface AutotelConfig {
|
|
|
1126
1143
|
*/
|
|
1127
1144
|
pretty?: boolean;
|
|
1128
1145
|
};
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Suppress console output while keeping OTel exporters running.
|
|
1149
|
+
* Useful for platforms like GCP Cloud Run / AWS Lambda where stdout
|
|
1150
|
+
* is managed externally by the platform's log collector.
|
|
1151
|
+
*
|
|
1152
|
+
* @default false
|
|
1153
|
+
*/
|
|
1154
|
+
silent?: boolean;
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Minimum log level for internal autotel diagnostic messages.
|
|
1158
|
+
* Messages below this level are dropped before processing.
|
|
1159
|
+
*
|
|
1160
|
+
* @default 'info'
|
|
1161
|
+
*/
|
|
1162
|
+
minLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
1129
1163
|
}
|
|
1130
1164
|
|
|
1131
1165
|
// Internal state
|
|
1132
1166
|
let initialized = false;
|
|
1167
|
+
let locked = false;
|
|
1133
1168
|
let config: AutotelConfig | null = null;
|
|
1134
1169
|
let sdk: NodeSDK | null = null;
|
|
1135
1170
|
let warnedOnce = false;
|
|
@@ -1138,6 +1173,86 @@ let validationConfig: Partial<ValidationConfig> | null = null;
|
|
|
1138
1173
|
let eventsConfig: EventsConfig | null = null;
|
|
1139
1174
|
let _stringRedactor: StringRedactor | null = null;
|
|
1140
1175
|
let _optionalRequire: typeof safeRequire = safeRequire;
|
|
1176
|
+
let _devtoolsClose: (() => Promise<void> | void) | null = null;
|
|
1177
|
+
|
|
1178
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } as const;
|
|
1179
|
+
type LogLevelKey = keyof typeof LOG_LEVELS;
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Lock the logger to prevent further `init()` calls.
|
|
1183
|
+
* Use this when framework plugins set up instrumentation and you want
|
|
1184
|
+
* to prevent accidental re-initialization from user code.
|
|
1185
|
+
*/
|
|
1186
|
+
export function lockLogger(): void {
|
|
1187
|
+
locked = true;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Check if the logger has been locked.
|
|
1192
|
+
*/
|
|
1193
|
+
export function isLoggerLocked(): boolean {
|
|
1194
|
+
return locked;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function createSilentLogger(): Logger {
|
|
1198
|
+
return {
|
|
1199
|
+
info: () => {},
|
|
1200
|
+
warn: () => {},
|
|
1201
|
+
error: () => {},
|
|
1202
|
+
debug: () => {},
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function wrapLogger(
|
|
1207
|
+
base: Logger,
|
|
1208
|
+
silent: boolean,
|
|
1209
|
+
minLevel: LogLevelKey,
|
|
1210
|
+
): Logger {
|
|
1211
|
+
if (silent) return createSilentLogger();
|
|
1212
|
+
const threshold = LOG_LEVELS[minLevel];
|
|
1213
|
+
const wrap = (fn: Logger['info'], level: LogLevelKey): Logger['info'] => {
|
|
1214
|
+
if (LOG_LEVELS[level] < threshold) {
|
|
1215
|
+
return (() => {}) as Logger['info'];
|
|
1216
|
+
}
|
|
1217
|
+
return ((...args: Parameters<Logger['info']>) =>
|
|
1218
|
+
fn(...args)) as Logger['info'];
|
|
1219
|
+
};
|
|
1220
|
+
return {
|
|
1221
|
+
debug: wrap(base.debug, 'debug'),
|
|
1222
|
+
info: wrap(base.info, 'info'),
|
|
1223
|
+
warn: wrap(base.warn, 'warn'),
|
|
1224
|
+
error: wrap(base.error, 'error'),
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function detectEnvironmentAttributes(): Record<string, string> {
|
|
1229
|
+
const attrs: Record<string, string> = {};
|
|
1230
|
+
|
|
1231
|
+
const commitSha =
|
|
1232
|
+
process.env.COMMIT_SHA ||
|
|
1233
|
+
process.env.GITHUB_SHA ||
|
|
1234
|
+
process.env.VERCEL_GIT_COMMIT_SHA ||
|
|
1235
|
+
process.env.CF_PAGES_COMMIT_SHA ||
|
|
1236
|
+
process.env.AWS_CODEPIPELINE_EXECUTION_ID;
|
|
1237
|
+
if (commitSha) attrs['service.commit.sha'] = commitSha;
|
|
1238
|
+
|
|
1239
|
+
const region =
|
|
1240
|
+
process.env.VERCEL_REGION ||
|
|
1241
|
+
process.env.AWS_REGION ||
|
|
1242
|
+
process.env.AWS_DEFAULT_REGION ||
|
|
1243
|
+
process.env.FLY_REGION ||
|
|
1244
|
+
process.env.CF_REGION ||
|
|
1245
|
+
process.env.GOOGLE_CLOUD_REGION;
|
|
1246
|
+
if (region) attrs['service.region'] = region;
|
|
1247
|
+
|
|
1248
|
+
const version =
|
|
1249
|
+
process.env.APP_VERSION ||
|
|
1250
|
+
process.env.HEROKU_RELEASE_VERSION ||
|
|
1251
|
+
process.env.VERCEL_GIT_COMMIT_REF;
|
|
1252
|
+
if (version) attrs['service.deploy.version'] = version;
|
|
1253
|
+
|
|
1254
|
+
return attrs;
|
|
1255
|
+
}
|
|
1141
1256
|
|
|
1142
1257
|
/**
|
|
1143
1258
|
* Resolve metrics flag with env var override support
|
|
@@ -1278,6 +1393,10 @@ function normalizeOtlpHeaders(
|
|
|
1278
1393
|
*/
|
|
1279
1394
|
|
|
1280
1395
|
export function init(cfg: AutotelConfig): void {
|
|
1396
|
+
if (locked) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1281
1400
|
// Resolve configs in priority order: explicit > yaml > env > defaults
|
|
1282
1401
|
const envConfig = resolveConfigFromEnv();
|
|
1283
1402
|
const yamlConfig = loadYamlConfig() ?? {};
|
|
@@ -1291,14 +1410,32 @@ export function init(cfg: AutotelConfig): void {
|
|
|
1291
1410
|
resourceAttributes: {
|
|
1292
1411
|
...envConfig.resourceAttributes,
|
|
1293
1412
|
...yamlConfig.resourceAttributes,
|
|
1413
|
+
...detectEnvironmentAttributes(),
|
|
1294
1414
|
...cfg.resourceAttributes,
|
|
1295
1415
|
},
|
|
1296
1416
|
// Handle headers merge (can be string or object)
|
|
1297
1417
|
headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers,
|
|
1298
1418
|
} as AutotelConfig;
|
|
1299
1419
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1420
|
+
if (mergedConfig.attributeRedactor !== undefined) {
|
|
1421
|
+
const normalizedRedactor = normalizeAttributeRedactorConfig(
|
|
1422
|
+
mergedConfig.attributeRedactor,
|
|
1423
|
+
);
|
|
1424
|
+
if (!normalizedRedactor) {
|
|
1425
|
+
throw new Error('Invalid attributeRedactor config');
|
|
1426
|
+
}
|
|
1427
|
+
mergedConfig.attributeRedactor = normalizedRedactor;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
|
|
1431
|
+
if (devtoolsConfig.enabled && mergedConfig.logs === undefined) {
|
|
1432
|
+
mergedConfig.logs = true;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const silent = mergedConfig.silent ?? false;
|
|
1436
|
+
const minLevel = mergedConfig.minLevel ?? 'info';
|
|
1437
|
+
const baseLogger = mergedConfig.logger || silentLogger;
|
|
1438
|
+
logger = wrapLogger(baseLogger, silent, minLevel);
|
|
1302
1439
|
|
|
1303
1440
|
// Warn if re-initializing (same behavior in all environments)
|
|
1304
1441
|
if (initialized) {
|
|
@@ -1314,7 +1451,7 @@ export function init(cfg: AutotelConfig): void {
|
|
|
1314
1451
|
|
|
1315
1452
|
// Initialize OpenTelemetry
|
|
1316
1453
|
// Only use endpoint if explicitly configured (no default fallback)
|
|
1317
|
-
|
|
1454
|
+
let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
|
|
1318
1455
|
const otlpHeaders = normalizeOtlpHeaders(mergedConfig.headers);
|
|
1319
1456
|
const version = mergedConfig.version || detectVersion();
|
|
1320
1457
|
const environment =
|
|
@@ -1322,6 +1459,35 @@ export function init(cfg: AutotelConfig): void {
|
|
|
1322
1459
|
const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
|
|
1323
1460
|
const logsEnabled = resolveLogsFlag(mergedConfig.logs);
|
|
1324
1461
|
|
|
1462
|
+
if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
|
|
1463
|
+
const devtoolsModule = _optionalRequire<{
|
|
1464
|
+
createDevtools?: (options?: {
|
|
1465
|
+
port?: number;
|
|
1466
|
+
host?: string;
|
|
1467
|
+
verbose?: boolean;
|
|
1468
|
+
}) => { port: number; close: () => Promise<void> | void };
|
|
1469
|
+
}>('autotel-devtools');
|
|
1470
|
+
|
|
1471
|
+
if (devtoolsModule?.createDevtools) {
|
|
1472
|
+
const devtoolsInstance = devtoolsModule.createDevtools({
|
|
1473
|
+
port: devtoolsConfig.port,
|
|
1474
|
+
host: devtoolsConfig.host,
|
|
1475
|
+
verbose: devtoolsConfig.verbose,
|
|
1476
|
+
});
|
|
1477
|
+
_devtoolsClose = devtoolsInstance.close;
|
|
1478
|
+
endpoint = `http://${devtoolsConfig.host}:${devtoolsInstance.port}`;
|
|
1479
|
+
logger.info(
|
|
1480
|
+
{},
|
|
1481
|
+
`[autotel] autotel-devtools embedded server started at ${endpoint}`,
|
|
1482
|
+
);
|
|
1483
|
+
} else {
|
|
1484
|
+
logger.warn(
|
|
1485
|
+
{},
|
|
1486
|
+
'[autotel] devtools.embedded requested but autotel-devtools is not installed. Falling back to endpoint-only mode.',
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1325
1491
|
// Detect hostname for proper Datadog correlation and Service Catalog discovery
|
|
1326
1492
|
const hostname = detectHostname();
|
|
1327
1493
|
|
|
@@ -2013,6 +2179,25 @@ export function _resetOptionalRequireForTesting(): void {
|
|
|
2013
2179
|
_optionalRequire = safeRequire;
|
|
2014
2180
|
}
|
|
2015
2181
|
|
|
2182
|
+
/**
|
|
2183
|
+
* @internal Close embedded devtools if running.
|
|
2184
|
+
*/
|
|
2185
|
+
export async function _closeEmbeddedDevtools(): Promise<void> {
|
|
2186
|
+
if (_devtoolsClose) {
|
|
2187
|
+
await _devtoolsClose();
|
|
2188
|
+
_devtoolsClose = null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* @internal Get embedded devtools close handle.
|
|
2194
|
+
*/
|
|
2195
|
+
export function _getEmbeddedDevtoolsCloseForTesting():
|
|
2196
|
+
| (() => Promise<void> | void)
|
|
2197
|
+
| null {
|
|
2198
|
+
return _devtoolsClose;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2016
2201
|
/**
|
|
2017
2202
|
* Get SDK instance (for shutdown)
|
|
2018
2203
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { trace as otelTrace } from '@opentelemetry/api';
|
|
2
3
|
import { getRequestLogger, runWithRequestContext } from './request-logger';
|
|
3
4
|
import type { TraceContext } from './trace-context';
|
|
4
5
|
|
|
@@ -23,6 +24,238 @@ function createMockContext(): TraceContext {
|
|
|
23
24
|
} as unknown as TraceContext;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('post-emit warnings', () => {
|
|
32
|
+
let ctx: TraceContext;
|
|
33
|
+
let log: ReturnType<typeof getRequestLogger>;
|
|
34
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
ctx = createMockContext();
|
|
38
|
+
log = getRequestLogger(ctx);
|
|
39
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
warnSpy.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('warns when set() is called after emitNow()', () => {
|
|
47
|
+
log.emitNow();
|
|
48
|
+
log.set({ dropped: 'value' });
|
|
49
|
+
|
|
50
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
51
|
+
expect.stringContaining(
|
|
52
|
+
'[autotel] log.set() called after the wide event was emitted',
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
56
|
+
expect.stringContaining('Keys dropped: dropped'),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('warns when info() is called after emitNow()', () => {
|
|
61
|
+
log.emitNow();
|
|
62
|
+
log.info('after emit', { extra: 'data' });
|
|
63
|
+
|
|
64
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringContaining(
|
|
66
|
+
'[autotel] log.info() called after the wide event was emitted',
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
70
|
+
expect.stringContaining('Keys dropped: message, extra'),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('warns when warn() is called after emitNow()', () => {
|
|
75
|
+
log.emitNow();
|
|
76
|
+
log.warn('after emit', { extra: 'data' });
|
|
77
|
+
|
|
78
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
79
|
+
expect.stringContaining(
|
|
80
|
+
'[autotel] log.warn() called after the wide event was emitted',
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('warns when error() is called after emitNow()', () => {
|
|
86
|
+
log.emitNow();
|
|
87
|
+
log.error(new Error('after emit'), { step: 'cleanup' });
|
|
88
|
+
|
|
89
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining(
|
|
91
|
+
'[autotel] log.error() called after the wide event was emitted',
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
95
|
+
expect.stringContaining('Keys dropped: step, error'),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('warns on duplicate emitNow()', () => {
|
|
100
|
+
const first = log.emitNow();
|
|
101
|
+
const second = log.emitNow();
|
|
102
|
+
|
|
103
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
104
|
+
expect.stringContaining(
|
|
105
|
+
'[autotel] log.emitNow() called after the wide event was emitted',
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
109
|
+
expect.stringContaining('Ignoring duplicate emit'),
|
|
110
|
+
);
|
|
111
|
+
expect(second).toBe(first);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('does not warn when calls are made before emitNow()', () => {
|
|
115
|
+
log.set({ user: { id: 'u1' } });
|
|
116
|
+
log.info('step', { stage: 'payment' });
|
|
117
|
+
log.warn('slow');
|
|
118
|
+
log.error(new Error('fail'));
|
|
119
|
+
log.emitNow();
|
|
120
|
+
|
|
121
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('array concatenation in set()', () => {
|
|
126
|
+
let ctx: TraceContext;
|
|
127
|
+
let log: ReturnType<typeof getRequestLogger>;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
ctx = createMockContext();
|
|
131
|
+
log = getRequestLogger(ctx);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('concatenates arrays when both values are arrays', () => {
|
|
135
|
+
log.set({ tags: ['initial', 'setup'] });
|
|
136
|
+
log.set({ tags: ['payment', 'complete'] });
|
|
137
|
+
|
|
138
|
+
expect(log.getContext()).toEqual({
|
|
139
|
+
tags: ['initial', 'setup', 'payment', 'complete'],
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('replaces array with non-array value', () => {
|
|
144
|
+
log.set({ tags: ['a', 'b'] });
|
|
145
|
+
log.set({ tags: 'single' });
|
|
146
|
+
|
|
147
|
+
expect(log.getContext()).toEqual({
|
|
148
|
+
tags: 'single',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('replaces non-array with array', () => {
|
|
153
|
+
log.set({ tags: 'single' });
|
|
154
|
+
log.set({ tags: ['a', 'b'] });
|
|
155
|
+
|
|
156
|
+
expect(log.getContext()).toEqual({
|
|
157
|
+
tags: ['a', 'b'],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('deep merges objects while concatenating nested arrays', () => {
|
|
162
|
+
log.set({
|
|
163
|
+
user: { id: 'u1', roles: ['admin'] },
|
|
164
|
+
});
|
|
165
|
+
log.set({
|
|
166
|
+
user: { name: 'Alice', roles: ['editor'] },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(log.getContext()).toEqual({
|
|
170
|
+
user: {
|
|
171
|
+
id: 'u1',
|
|
172
|
+
name: 'Alice',
|
|
173
|
+
roles: ['admin', 'editor'],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('log.fork()', () => {
|
|
180
|
+
let ctx: TraceContext;
|
|
181
|
+
let log: ReturnType<typeof getRequestLogger>;
|
|
182
|
+
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
ctx = createMockContext();
|
|
185
|
+
log = getRequestLogger(ctx);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('throws when parent has no correlationId', () => {
|
|
189
|
+
const noCorrCtx = createMockContext();
|
|
190
|
+
(noCorrCtx as Record<string, unknown>).correlationId = '';
|
|
191
|
+
const noCorrLog = getRequestLogger(noCorrCtx);
|
|
192
|
+
|
|
193
|
+
expect(() => noCorrLog.fork('test', () => {})).toThrow(
|
|
194
|
+
'[autotel] log.fork() requires the parent logger to have a correlationId',
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('fork method exists on the logger interface', () => {
|
|
199
|
+
expect(typeof log.fork).toBe('function');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('uses an isolated child span context for forked work', async () => {
|
|
203
|
+
const childSpan = {
|
|
204
|
+
spanContext: () => ({
|
|
205
|
+
traceId: 'a'.repeat(32),
|
|
206
|
+
spanId: 'b'.repeat(16),
|
|
207
|
+
}),
|
|
208
|
+
setAttribute: vi.fn(),
|
|
209
|
+
setAttributes: vi.fn(),
|
|
210
|
+
setStatus: vi.fn(),
|
|
211
|
+
recordException: vi.fn(),
|
|
212
|
+
addEvent: vi.fn(),
|
|
213
|
+
addLink: vi.fn(),
|
|
214
|
+
addLinks: vi.fn(),
|
|
215
|
+
updateName: vi.fn(),
|
|
216
|
+
isRecording: vi.fn(() => true),
|
|
217
|
+
end: vi.fn(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const tracer = {
|
|
221
|
+
startActiveSpan: (
|
|
222
|
+
_name: string,
|
|
223
|
+
cb: (span: typeof childSpan) => void,
|
|
224
|
+
) => {
|
|
225
|
+
cb(childSpan);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const tracerSpy = vi
|
|
230
|
+
.spyOn(otelTrace, 'getTracer')
|
|
231
|
+
.mockReturnValue(
|
|
232
|
+
tracer as unknown as ReturnType<typeof otelTrace.getTracer>,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
log.fork('background-work', async () => {
|
|
236
|
+
const childLog = getRequestLogger();
|
|
237
|
+
childLog.info('running child task', { phase: 'upload' });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
241
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
242
|
+
|
|
243
|
+
expect(tracerSpy).toHaveBeenCalledWith('autotel.request-logger');
|
|
244
|
+
expect(childSpan.setAttributes).toHaveBeenCalledWith(
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
operation: 'background-work',
|
|
247
|
+
_parentCorrelationId: 'corr-id',
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
expect(childSpan.addEvent).toHaveBeenCalledWith(
|
|
251
|
+
'log.emit.manual',
|
|
252
|
+
expect.any(Object),
|
|
253
|
+
);
|
|
254
|
+
expect(ctx.setAttributes).not.toHaveBeenCalled();
|
|
255
|
+
expect(childSpan.end).toHaveBeenCalledTimes(1);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
26
259
|
describe('getRequestLogger', () => {
|
|
27
260
|
it('sets flattened fields onto active span context', () => {
|
|
28
261
|
const ctx = createMockContext();
|
|
@@ -134,4 +367,36 @@ describe('getRequestLogger', () => {
|
|
|
134
367
|
});
|
|
135
368
|
});
|
|
136
369
|
});
|
|
370
|
+
|
|
371
|
+
it('resolves context from the active OTel span when no args and no ALS context', () => {
|
|
372
|
+
const activeSpan = {
|
|
373
|
+
spanContext: () => ({
|
|
374
|
+
traceId: 'c'.repeat(32),
|
|
375
|
+
spanId: 'd'.repeat(16),
|
|
376
|
+
}),
|
|
377
|
+
setAttribute: vi.fn(),
|
|
378
|
+
setAttributes: vi.fn(),
|
|
379
|
+
setStatus: vi.fn(),
|
|
380
|
+
recordException: vi.fn(),
|
|
381
|
+
addEvent: vi.fn(),
|
|
382
|
+
addLink: vi.fn(),
|
|
383
|
+
addLinks: vi.fn(),
|
|
384
|
+
updateName: vi.fn(),
|
|
385
|
+
isRecording: vi.fn(() => true),
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const spanSpy = vi
|
|
389
|
+
.spyOn(otelTrace, 'getActiveSpan')
|
|
390
|
+
.mockReturnValue(
|
|
391
|
+
activeSpan as unknown as ReturnType<typeof otelTrace.getActiveSpan>,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const log = getRequestLogger();
|
|
395
|
+
log.set({ order: { id: 'o-1' } });
|
|
396
|
+
|
|
397
|
+
expect(spanSpy).toHaveBeenCalled();
|
|
398
|
+
expect(activeSpan.setAttributes).toHaveBeenCalledWith({
|
|
399
|
+
'order.id': 'o-1',
|
|
400
|
+
});
|
|
401
|
+
});
|
|
137
402
|
});
|