@aichatwar/shared 1.0.145 → 1.0.147
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/build/index.d.ts +5 -0
- package/build/index.js +6 -0
- package/build/middlewares/error-handler.js +11 -1
- package/build/observability/correlation.d.ts +6 -0
- package/build/observability/correlation.js +33 -0
- package/build/observability/express.d.ts +3 -0
- package/build/observability/express.js +39 -0
- package/build/observability/logger.d.ts +2 -0
- package/build/observability/logger.js +49 -0
- package/build/observability/third-party-logging.d.ts +101 -0
- package/build/observability/third-party-logging.js +212 -0
- package/build/observability/tracing.d.ts +51 -0
- package/build/observability/tracing.js +211 -0
- package/package.json +9 -2
package/build/index.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export * from "./middlewares/error-handler";
|
|
|
8
8
|
export * from "./middlewares/jwt-extractor";
|
|
9
9
|
export * from "./middlewares/login-required";
|
|
10
10
|
export * from "./middlewares/validate-request";
|
|
11
|
+
export * from "./observability/logger";
|
|
12
|
+
export * from "./observability/correlation";
|
|
13
|
+
export * from "./observability/express";
|
|
14
|
+
export * from "./observability/tracing";
|
|
15
|
+
export * from "./observability/third-party-logging";
|
|
11
16
|
export * from "./events/nats/baseListener";
|
|
12
17
|
export * from "./events/nats/basePublisher";
|
|
13
18
|
export * from "./events/kafka/baseListener";
|
package/build/index.js
CHANGED
|
@@ -24,6 +24,12 @@ __exportStar(require("./middlewares/error-handler"), exports);
|
|
|
24
24
|
__exportStar(require("./middlewares/jwt-extractor"), exports);
|
|
25
25
|
__exportStar(require("./middlewares/login-required"), exports);
|
|
26
26
|
__exportStar(require("./middlewares/validate-request"), exports);
|
|
27
|
+
// Observability (Phase 1)
|
|
28
|
+
__exportStar(require("./observability/logger"), exports);
|
|
29
|
+
__exportStar(require("./observability/correlation"), exports);
|
|
30
|
+
__exportStar(require("./observability/express"), exports);
|
|
31
|
+
__exportStar(require("./observability/tracing"), exports);
|
|
32
|
+
__exportStar(require("./observability/third-party-logging"), exports);
|
|
27
33
|
__exportStar(require("./events/nats/baseListener"), exports);
|
|
28
34
|
__exportStar(require("./events/nats/basePublisher"), exports);
|
|
29
35
|
__exportStar(require("./events/kafka/baseListener"), exports);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.errorHandler = void 0;
|
|
4
4
|
const customError_1 = require("../errors/customError");
|
|
5
|
+
const correlation_1 = require("../observability/correlation");
|
|
6
|
+
const logger_1 = require("../observability/logger");
|
|
5
7
|
`common structure for errors:
|
|
6
8
|
{
|
|
7
9
|
errors:{
|
|
@@ -10,7 +12,15 @@ const customError_1 = require("../errors/customError");
|
|
|
10
12
|
}
|
|
11
13
|
`;
|
|
12
14
|
const errorHandler = (err, req, res, next) => {
|
|
13
|
-
|
|
15
|
+
const correlationId = (0, correlation_1.getCorrelationId)();
|
|
16
|
+
(0, logger_1.logger)().error({
|
|
17
|
+
correlationId,
|
|
18
|
+
err,
|
|
19
|
+
http: {
|
|
20
|
+
method: req.method,
|
|
21
|
+
path: req.originalUrl || req.url,
|
|
22
|
+
},
|
|
23
|
+
}, "request.error");
|
|
14
24
|
if (err instanceof customError_1.CustomError) {
|
|
15
25
|
return res.status(err.statusCode).send({ errors: err.serializeDetails() });
|
|
16
26
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type CorrelationContext = {
|
|
2
|
+
correlationId: string;
|
|
3
|
+
};
|
|
4
|
+
export declare function runWithCorrelationId<T>(correlationId: string | undefined, fn: () => T): T;
|
|
5
|
+
export declare function getCorrelationId(): string | undefined;
|
|
6
|
+
export declare function getOrCreateCorrelationId(incoming?: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runWithCorrelationId = runWithCorrelationId;
|
|
7
|
+
exports.getCorrelationId = getCorrelationId;
|
|
8
|
+
exports.getOrCreateCorrelationId = getOrCreateCorrelationId;
|
|
9
|
+
const async_hooks_1 = require("async_hooks");
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const storage = new async_hooks_1.AsyncLocalStorage();
|
|
12
|
+
function newCorrelationId() {
|
|
13
|
+
// Prefer Node's built-in UUID when available.
|
|
14
|
+
const anyCrypto = crypto_1.default;
|
|
15
|
+
if (typeof anyCrypto.randomUUID === 'function') {
|
|
16
|
+
return anyCrypto.randomUUID();
|
|
17
|
+
}
|
|
18
|
+
return crypto_1.default.randomBytes(16).toString('hex');
|
|
19
|
+
}
|
|
20
|
+
function runWithCorrelationId(correlationId, fn) {
|
|
21
|
+
const cid = correlationId && String(correlationId).trim() ? String(correlationId).trim() : newCorrelationId();
|
|
22
|
+
return storage.run({ correlationId: cid }, fn);
|
|
23
|
+
}
|
|
24
|
+
function getCorrelationId() {
|
|
25
|
+
var _a;
|
|
26
|
+
return (_a = storage.getStore()) === null || _a === void 0 ? void 0 : _a.correlationId;
|
|
27
|
+
}
|
|
28
|
+
function getOrCreateCorrelationId(incoming) {
|
|
29
|
+
const existing = getCorrelationId();
|
|
30
|
+
if (existing)
|
|
31
|
+
return existing;
|
|
32
|
+
return (incoming && String(incoming).trim()) || newCorrelationId();
|
|
33
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
export declare function correlationIdMiddleware(req: Request, res: Response, next: NextFunction): void;
|
|
3
|
+
export declare function requestLoggingMiddleware(req: Request, res: Response, next: NextFunction): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.correlationIdMiddleware = correlationIdMiddleware;
|
|
4
|
+
exports.requestLoggingMiddleware = requestLoggingMiddleware;
|
|
5
|
+
const correlation_1 = require("./correlation");
|
|
6
|
+
const logger_1 = require("./logger");
|
|
7
|
+
function correlationIdMiddleware(req, res, next) {
|
|
8
|
+
const incoming = req.headers['x-correlation-id'] || req.headers['x-request-id'];
|
|
9
|
+
(0, correlation_1.runWithCorrelationId)(incoming, () => {
|
|
10
|
+
const cid = (0, correlation_1.getCorrelationId)();
|
|
11
|
+
if (cid) {
|
|
12
|
+
res.setHeader('x-correlation-id', cid);
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function requestLoggingMiddleware(req, res, next) {
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
const cid = () => (0, correlation_1.getCorrelationId)();
|
|
20
|
+
// Log at finish so that downstream middleware (e.g. JWT extractors) can attach context to req.
|
|
21
|
+
res.on('finish', () => {
|
|
22
|
+
const durationMs = Date.now() - start;
|
|
23
|
+
const statusCode = res.statusCode;
|
|
24
|
+
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
|
|
25
|
+
const jwtPayload = req.jwtPayload;
|
|
26
|
+
const userId = (jwtPayload === null || jwtPayload === void 0 ? void 0 : jwtPayload.id) ? String(jwtPayload.id) : undefined;
|
|
27
|
+
(0, logger_1.logger)()[level]({
|
|
28
|
+
correlationId: cid(),
|
|
29
|
+
http: {
|
|
30
|
+
method: req.method,
|
|
31
|
+
path: req.originalUrl || req.url,
|
|
32
|
+
statusCode,
|
|
33
|
+
durationMs,
|
|
34
|
+
},
|
|
35
|
+
userId,
|
|
36
|
+
}, 'http.request');
|
|
37
|
+
});
|
|
38
|
+
next();
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = logger;
|
|
7
|
+
const pino_1 = __importDefault(require("pino"));
|
|
8
|
+
let _logger = null;
|
|
9
|
+
function buildLogger() {
|
|
10
|
+
const isDev = (process.env.NODE_ENV || '').toLowerCase() !== 'production';
|
|
11
|
+
const base = {
|
|
12
|
+
service: process.env.SERVICE_NAME,
|
|
13
|
+
environment: process.env.NODE_ENV,
|
|
14
|
+
version: process.env.APP_VERSION,
|
|
15
|
+
};
|
|
16
|
+
// Keep redaction broad and safe. Services can add more redaction upstream.
|
|
17
|
+
const redactPaths = [
|
|
18
|
+
'req.headers.authorization',
|
|
19
|
+
'req.headers.cookie',
|
|
20
|
+
'req.headers["set-cookie"]',
|
|
21
|
+
'req.headers["x-api-key"]',
|
|
22
|
+
'password',
|
|
23
|
+
'token',
|
|
24
|
+
'accessToken',
|
|
25
|
+
'refreshToken',
|
|
26
|
+
'apiKey',
|
|
27
|
+
'authorization',
|
|
28
|
+
'cookie',
|
|
29
|
+
];
|
|
30
|
+
return (0, pino_1.default)({
|
|
31
|
+
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
|
|
32
|
+
timestamp: pino_1.default.stdTimeFunctions.isoTime,
|
|
33
|
+
base,
|
|
34
|
+
redact: { paths: redactPaths, remove: true },
|
|
35
|
+
formatters: {
|
|
36
|
+
level: (label) => ({ level: label }),
|
|
37
|
+
},
|
|
38
|
+
serializers: {
|
|
39
|
+
err: pino_1.default.stdSerializers.err,
|
|
40
|
+
req: pino_1.default.stdSerializers.req,
|
|
41
|
+
res: pino_1.default.stdSerializers.res,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function logger() {
|
|
46
|
+
if (!_logger)
|
|
47
|
+
_logger = buildLogger();
|
|
48
|
+
return _logger;
|
|
49
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized logging for 3rd-party API calls
|
|
3
|
+
*
|
|
4
|
+
* This module provides helpers for logging:
|
|
5
|
+
* - Retry attempts (WARN level)
|
|
6
|
+
* - Final failures (ERROR level)
|
|
7
|
+
* - Rate limits (WARN/ERROR based on impact)
|
|
8
|
+
* - Unmasked error details (stack traces, status codes, request IDs)
|
|
9
|
+
* - Correlation IDs for traceability
|
|
10
|
+
*
|
|
11
|
+
* Best practices:
|
|
12
|
+
* - Each retry: log as WARN with attempt, backoff, reason
|
|
13
|
+
* - Final failure: log as ERROR with full error context
|
|
14
|
+
* - Rate limits: log as WARN or ERROR depending on user impact
|
|
15
|
+
*/
|
|
16
|
+
export interface ThirdPartyErrorContext {
|
|
17
|
+
/** Provider name (e.g., 'openai', 'anthropic', 'cohere') */
|
|
18
|
+
provider: string;
|
|
19
|
+
/** Operation name (e.g., 'generateResponse', 'createAgent') */
|
|
20
|
+
operation: string;
|
|
21
|
+
/** HTTP status code if available */
|
|
22
|
+
statusCode?: number;
|
|
23
|
+
/** Error type/class */
|
|
24
|
+
errorType?: string;
|
|
25
|
+
/** Error message */
|
|
26
|
+
errorMessage: string;
|
|
27
|
+
/** Full error object (for stack trace) */
|
|
28
|
+
error: any;
|
|
29
|
+
/** Whether this error is retryable */
|
|
30
|
+
retryable?: boolean;
|
|
31
|
+
/** Request ID from upstream service if available */
|
|
32
|
+
requestId?: string;
|
|
33
|
+
/** Rate limit headers if available */
|
|
34
|
+
rateLimitHeaders?: {
|
|
35
|
+
limit?: string;
|
|
36
|
+
remaining?: string;
|
|
37
|
+
reset?: string;
|
|
38
|
+
};
|
|
39
|
+
/** Additional metadata */
|
|
40
|
+
metadata?: Record<string, any>;
|
|
41
|
+
}
|
|
42
|
+
export interface RetryAttemptContext {
|
|
43
|
+
/** Provider name */
|
|
44
|
+
provider: string;
|
|
45
|
+
/** Operation name */
|
|
46
|
+
operation: string;
|
|
47
|
+
/** Current attempt number (1-indexed) */
|
|
48
|
+
attempt: number;
|
|
49
|
+
/** Maximum number of attempts */
|
|
50
|
+
maxAttempts: number;
|
|
51
|
+
/** Backoff delay in milliseconds */
|
|
52
|
+
backoffMs: number;
|
|
53
|
+
/** Reason for retry */
|
|
54
|
+
reason: string;
|
|
55
|
+
/** Previous error that triggered retry */
|
|
56
|
+
previousError?: any;
|
|
57
|
+
/** HTTP status code from previous attempt */
|
|
58
|
+
previousStatusCode?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Log a retry attempt for a 3rd-party API call
|
|
62
|
+
*
|
|
63
|
+
* This should be called BEFORE each retry attempt.
|
|
64
|
+
* Logs at WARN level as per observability best practices.
|
|
65
|
+
*
|
|
66
|
+
* @param context Retry attempt context
|
|
67
|
+
*/
|
|
68
|
+
export declare function logRetryAttempt(context: RetryAttemptContext): void;
|
|
69
|
+
/**
|
|
70
|
+
* Log a final failure for a 3rd-party API call
|
|
71
|
+
*
|
|
72
|
+
* This should be called when all retries are exhausted or when a non-retryable error occurs.
|
|
73
|
+
* Logs at ERROR level with full unmasked error details.
|
|
74
|
+
*
|
|
75
|
+
* @param context Error context
|
|
76
|
+
*/
|
|
77
|
+
export declare function logThirdPartyError(context: ThirdPartyErrorContext): void;
|
|
78
|
+
/**
|
|
79
|
+
* Log a successful 3rd-party API call
|
|
80
|
+
*
|
|
81
|
+
* @param provider Provider name
|
|
82
|
+
* @param operation Operation name
|
|
83
|
+
* @param durationMs Duration in milliseconds
|
|
84
|
+
* @param metadata Additional metadata (e.g., token usage, request ID)
|
|
85
|
+
*/
|
|
86
|
+
export declare function logThirdPartySuccess(provider: string, operation: string, durationMs: number, metadata?: Record<string, any>): void;
|
|
87
|
+
/**
|
|
88
|
+
* Extract error context from a caught error
|
|
89
|
+
*
|
|
90
|
+
* Handles various error types:
|
|
91
|
+
* - Axios errors (with response.status, response.data)
|
|
92
|
+
* - OpenAI SDK errors (with status, message)
|
|
93
|
+
* - Anthropic SDK errors (with status, message)
|
|
94
|
+
* - Generic errors
|
|
95
|
+
*
|
|
96
|
+
* @param error Error object
|
|
97
|
+
* @param provider Provider name
|
|
98
|
+
* @param operation Operation name
|
|
99
|
+
* @returns Error context
|
|
100
|
+
*/
|
|
101
|
+
export declare function extractErrorContext(error: any, provider: string, operation: string): ThirdPartyErrorContext;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Standardized logging for 3rd-party API calls
|
|
4
|
+
*
|
|
5
|
+
* This module provides helpers for logging:
|
|
6
|
+
* - Retry attempts (WARN level)
|
|
7
|
+
* - Final failures (ERROR level)
|
|
8
|
+
* - Rate limits (WARN/ERROR based on impact)
|
|
9
|
+
* - Unmasked error details (stack traces, status codes, request IDs)
|
|
10
|
+
* - Correlation IDs for traceability
|
|
11
|
+
*
|
|
12
|
+
* Best practices:
|
|
13
|
+
* - Each retry: log as WARN with attempt, backoff, reason
|
|
14
|
+
* - Final failure: log as ERROR with full error context
|
|
15
|
+
* - Rate limits: log as WARN or ERROR depending on user impact
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.logRetryAttempt = logRetryAttempt;
|
|
19
|
+
exports.logThirdPartyError = logThirdPartyError;
|
|
20
|
+
exports.logThirdPartySuccess = logThirdPartySuccess;
|
|
21
|
+
exports.extractErrorContext = extractErrorContext;
|
|
22
|
+
const logger_1 = require("./logger");
|
|
23
|
+
const correlation_1 = require("./correlation");
|
|
24
|
+
const tracing_1 = require("./tracing");
|
|
25
|
+
/**
|
|
26
|
+
* Log a retry attempt for a 3rd-party API call
|
|
27
|
+
*
|
|
28
|
+
* This should be called BEFORE each retry attempt.
|
|
29
|
+
* Logs at WARN level as per observability best practices.
|
|
30
|
+
*
|
|
31
|
+
* @param context Retry attempt context
|
|
32
|
+
*/
|
|
33
|
+
function logRetryAttempt(context) {
|
|
34
|
+
var _a, _b, _c, _d, _e, _f;
|
|
35
|
+
const correlationId = (0, correlation_1.getCorrelationId)();
|
|
36
|
+
(0, logger_1.logger)().warn({
|
|
37
|
+
message: '3rd-party API retry attempt',
|
|
38
|
+
provider: context.provider,
|
|
39
|
+
operation: context.operation,
|
|
40
|
+
attempt: context.attempt,
|
|
41
|
+
maxAttempts: context.maxAttempts,
|
|
42
|
+
backoffMs: context.backoffMs,
|
|
43
|
+
reason: context.reason,
|
|
44
|
+
previousStatusCode: context.previousStatusCode,
|
|
45
|
+
correlationId,
|
|
46
|
+
// Include previous error details (unmasked) for debugging
|
|
47
|
+
previousError: context.previousError ? {
|
|
48
|
+
type: ((_b = (_a = context.previousError) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name) || typeof context.previousError,
|
|
49
|
+
message: (_c = context.previousError) === null || _c === void 0 ? void 0 : _c.message,
|
|
50
|
+
status: ((_d = context.previousError) === null || _d === void 0 ? void 0 : _d.status) || ((_e = context.previousError) === null || _e === void 0 ? void 0 : _e.statusCode),
|
|
51
|
+
code: (_f = context.previousError) === null || _f === void 0 ? void 0 : _f.code,
|
|
52
|
+
// Don't include full stack in retry log (will be in final error log)
|
|
53
|
+
} : undefined,
|
|
54
|
+
});
|
|
55
|
+
// Add retry attempt to span
|
|
56
|
+
(0, tracing_1.addSpanAttributes)({
|
|
57
|
+
[`${context.provider}.retry.attempt`]: context.attempt,
|
|
58
|
+
[`${context.provider}.retry.reason`]: context.reason,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Log a final failure for a 3rd-party API call
|
|
63
|
+
*
|
|
64
|
+
* This should be called when all retries are exhausted or when a non-retryable error occurs.
|
|
65
|
+
* Logs at ERROR level with full unmasked error details.
|
|
66
|
+
*
|
|
67
|
+
* @param context Error context
|
|
68
|
+
*/
|
|
69
|
+
function logThirdPartyError(context) {
|
|
70
|
+
var _a, _b, _c;
|
|
71
|
+
const correlationId = (0, correlation_1.getCorrelationId)();
|
|
72
|
+
// Determine log level: ERROR for failures, WARN for rate limits that don't block user
|
|
73
|
+
const isRateLimit = context.statusCode === 429;
|
|
74
|
+
const logLevel = isRateLimit && !context.retryable ? 'warn' : 'error';
|
|
75
|
+
// Build error log entry with unmasked details
|
|
76
|
+
const errorLog = Object.assign({ message: `3rd-party API ${context.operation} failed`, provider: context.provider, operation: context.operation, errorType: context.errorType || ((_b = (_a = context.error) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name) || 'UnknownError', errorMessage: context.errorMessage, statusCode: context.statusCode, retryable: context.retryable, correlationId,
|
|
77
|
+
// Include full error object for stack trace (unmasked)
|
|
78
|
+
err: context.error,
|
|
79
|
+
// Include request ID if available (for upstream debugging)
|
|
80
|
+
requestId: context.requestId,
|
|
81
|
+
// Include rate limit headers if available
|
|
82
|
+
rateLimit: context.rateLimitHeaders }, (context.metadata || {}));
|
|
83
|
+
// Log at appropriate level
|
|
84
|
+
if (logLevel === 'error') {
|
|
85
|
+
(0, logger_1.logger)().error(errorLog);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
(0, logger_1.logger)().warn(errorLog);
|
|
89
|
+
}
|
|
90
|
+
// Record exception in OpenTelemetry span
|
|
91
|
+
if (context.error instanceof Error) {
|
|
92
|
+
(0, tracing_1.recordSpanException)(context.error);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Create Error object if not already one
|
|
96
|
+
const errorObj = new Error(context.errorMessage);
|
|
97
|
+
if ((_c = context.error) === null || _c === void 0 ? void 0 : _c.stack) {
|
|
98
|
+
errorObj.stack = context.error.stack;
|
|
99
|
+
}
|
|
100
|
+
(0, tracing_1.recordSpanException)(errorObj);
|
|
101
|
+
}
|
|
102
|
+
// Add error attributes to span
|
|
103
|
+
(0, tracing_1.addSpanAttributes)({
|
|
104
|
+
[`${context.provider}.error`]: true,
|
|
105
|
+
[`${context.provider}.error.type`]: context.errorType || 'UnknownError',
|
|
106
|
+
[`${context.provider}.error.status_code`]: context.statusCode || 0,
|
|
107
|
+
[`${context.provider}.error.retryable`]: context.retryable || false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Log a successful 3rd-party API call
|
|
112
|
+
*
|
|
113
|
+
* @param provider Provider name
|
|
114
|
+
* @param operation Operation name
|
|
115
|
+
* @param durationMs Duration in milliseconds
|
|
116
|
+
* @param metadata Additional metadata (e.g., token usage, request ID)
|
|
117
|
+
*/
|
|
118
|
+
function logThirdPartySuccess(provider, operation, durationMs, metadata) {
|
|
119
|
+
const correlationId = (0, correlation_1.getCorrelationId)();
|
|
120
|
+
(0, logger_1.logger)().info(Object.assign({ message: `3rd-party API ${operation} succeeded`, provider,
|
|
121
|
+
operation, duration_ms: durationMs, correlationId }, (metadata || {})));
|
|
122
|
+
// Add success attributes to span
|
|
123
|
+
(0, tracing_1.addSpanAttributes)({
|
|
124
|
+
[`${provider}.success`]: true,
|
|
125
|
+
[`${provider}.duration_ms`]: durationMs,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract error context from a caught error
|
|
130
|
+
*
|
|
131
|
+
* Handles various error types:
|
|
132
|
+
* - Axios errors (with response.status, response.data)
|
|
133
|
+
* - OpenAI SDK errors (with status, message)
|
|
134
|
+
* - Anthropic SDK errors (with status, message)
|
|
135
|
+
* - Generic errors
|
|
136
|
+
*
|
|
137
|
+
* @param error Error object
|
|
138
|
+
* @param provider Provider name
|
|
139
|
+
* @param operation Operation name
|
|
140
|
+
* @returns Error context
|
|
141
|
+
*/
|
|
142
|
+
function extractErrorContext(error, provider, operation) {
|
|
143
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
144
|
+
// Extract status code from various error formats
|
|
145
|
+
let statusCode;
|
|
146
|
+
let requestId;
|
|
147
|
+
let rateLimitHeaders;
|
|
148
|
+
// Handle Axios errors
|
|
149
|
+
if (error.response) {
|
|
150
|
+
statusCode = error.response.status;
|
|
151
|
+
requestId = ((_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['x-request-id']) || ((_b = error.response.headers) === null || _b === void 0 ? void 0 : _b['request-id']);
|
|
152
|
+
rateLimitHeaders = {
|
|
153
|
+
limit: (_c = error.response.headers) === null || _c === void 0 ? void 0 : _c['x-ratelimit-limit'],
|
|
154
|
+
remaining: (_d = error.response.headers) === null || _d === void 0 ? void 0 : _d['x-ratelimit-remaining'],
|
|
155
|
+
reset: (_e = error.response.headers) === null || _e === void 0 ? void 0 : _e['x-ratelimit-reset'],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// Handle OpenAI/Anthropic SDK errors
|
|
159
|
+
else if (error.status) {
|
|
160
|
+
statusCode = error.status;
|
|
161
|
+
requestId = error.request_id || error.requestId;
|
|
162
|
+
}
|
|
163
|
+
// Handle HTTP errors with statusCode
|
|
164
|
+
else if (error.statusCode) {
|
|
165
|
+
statusCode = error.statusCode;
|
|
166
|
+
}
|
|
167
|
+
// Determine if error is retryable
|
|
168
|
+
const retryable = isRetryableError(statusCode, error.code);
|
|
169
|
+
// Extract error message
|
|
170
|
+
const errorMessage = error.message ||
|
|
171
|
+
((_h = (_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.data) === null || _g === void 0 ? void 0 : _g.error) === null || _h === void 0 ? void 0 : _h.message) ||
|
|
172
|
+
((_k = (_j = error.response) === null || _j === void 0 ? void 0 : _j.data) === null || _k === void 0 ? void 0 : _k.message) ||
|
|
173
|
+
'Unknown error';
|
|
174
|
+
return {
|
|
175
|
+
provider,
|
|
176
|
+
operation,
|
|
177
|
+
statusCode,
|
|
178
|
+
errorType: ((_l = error.constructor) === null || _l === void 0 ? void 0 : _l.name) || error.type || 'UnknownError',
|
|
179
|
+
errorMessage,
|
|
180
|
+
error,
|
|
181
|
+
retryable,
|
|
182
|
+
requestId,
|
|
183
|
+
rateLimitHeaders,
|
|
184
|
+
metadata: {
|
|
185
|
+
code: error.code,
|
|
186
|
+
type: error.type,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Determine if an error is retryable based on status code and error code
|
|
192
|
+
*
|
|
193
|
+
* @param statusCode HTTP status code
|
|
194
|
+
* @param errorCode Error code (e.g., 'ECONNREFUSED', 'ETIMEDOUT')
|
|
195
|
+
* @returns true if error is retryable
|
|
196
|
+
*/
|
|
197
|
+
function isRetryableError(statusCode, errorCode) {
|
|
198
|
+
// Retryable HTTP status codes
|
|
199
|
+
if (statusCode === 429 || statusCode === 500 || statusCode === 502 || statusCode === 503 || statusCode === 504) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
// Retryable network errors
|
|
203
|
+
if (errorCode === 'ECONNREFUSED' || errorCode === 'ETIMEDOUT' || errorCode === 'ENOTFOUND') {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
// Non-retryable: authentication, invalid requests, not found
|
|
207
|
+
if (statusCode === 401 || statusCode === 400 || statusCode === 404) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
// Default to retryable for unknown errors (conservative approach)
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry tracing setup with Azure Monitor exporter
|
|
3
|
+
*
|
|
4
|
+
* This module initializes OpenTelemetry SDK with:
|
|
5
|
+
* - Azure Monitor exporter for Application Insights
|
|
6
|
+
* - Auto-instrumentation for HTTP, MongoDB, Redis, Kafka
|
|
7
|
+
* - Correlation ID propagation
|
|
8
|
+
* - Service name and version from environment
|
|
9
|
+
*/
|
|
10
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
11
|
+
import { Span } from '@opentelemetry/api';
|
|
12
|
+
/**
|
|
13
|
+
* Initialize OpenTelemetry SDK with Azure Monitor exporter
|
|
14
|
+
*
|
|
15
|
+
* Environment variables:
|
|
16
|
+
* - APPLICATIONINSIGHTS_CONNECTION_STRING: Azure Application Insights connection string
|
|
17
|
+
* - SERVICE_NAME: Service name (default: 'unknown-service')
|
|
18
|
+
* - APP_VERSION: Service version (default: 'unknown')
|
|
19
|
+
* - OTEL_SERVICE_NAME: OpenTelemetry service name (falls back to SERVICE_NAME)
|
|
20
|
+
* - OTEL_LOG_LEVEL: OpenTelemetry log level (default: 'info')
|
|
21
|
+
*
|
|
22
|
+
* @returns NodeSDK instance or null if initialization fails
|
|
23
|
+
*/
|
|
24
|
+
export declare function initializeTracing(): NodeSDK | null;
|
|
25
|
+
/**
|
|
26
|
+
* Shutdown OpenTelemetry SDK gracefully
|
|
27
|
+
* Call this during service shutdown
|
|
28
|
+
*/
|
|
29
|
+
export declare function shutdownTracing(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Get the current active span
|
|
32
|
+
* @returns Active span or undefined
|
|
33
|
+
*/
|
|
34
|
+
export declare function getActiveSpan(): Span | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Create a new span for an operation
|
|
37
|
+
* @param name Span name
|
|
38
|
+
* @param fn Function to execute within the span
|
|
39
|
+
* @returns Result of the function
|
|
40
|
+
*/
|
|
41
|
+
export declare function withSpan<T>(name: string, fn: (span: Span) => Promise<T> | T): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Add attributes to the current active span
|
|
44
|
+
* @param attributes Key-value pairs to add
|
|
45
|
+
*/
|
|
46
|
+
export declare function addSpanAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Record an exception on the current active span
|
|
49
|
+
* @param error Error to record
|
|
50
|
+
*/
|
|
51
|
+
export declare function recordSpanException(error: Error): void;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenTelemetry tracing setup with Azure Monitor exporter
|
|
4
|
+
*
|
|
5
|
+
* This module initializes OpenTelemetry SDK with:
|
|
6
|
+
* - Azure Monitor exporter for Application Insights
|
|
7
|
+
* - Auto-instrumentation for HTTP, MongoDB, Redis, Kafka
|
|
8
|
+
* - Correlation ID propagation
|
|
9
|
+
* - Service name and version from environment
|
|
10
|
+
*/
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.initializeTracing = initializeTracing;
|
|
22
|
+
exports.shutdownTracing = shutdownTracing;
|
|
23
|
+
exports.getActiveSpan = getActiveSpan;
|
|
24
|
+
exports.withSpan = withSpan;
|
|
25
|
+
exports.addSpanAttributes = addSpanAttributes;
|
|
26
|
+
exports.recordSpanException = recordSpanException;
|
|
27
|
+
const sdk_node_1 = require("@opentelemetry/sdk-node");
|
|
28
|
+
const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
|
|
29
|
+
const monitor_opentelemetry_exporter_1 = require("@azure/monitor-opentelemetry-exporter");
|
|
30
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
31
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
32
|
+
const api_1 = require("@opentelemetry/api");
|
|
33
|
+
const correlation_1 = require("./correlation");
|
|
34
|
+
const logger_1 = require("./logger");
|
|
35
|
+
let sdk = null;
|
|
36
|
+
/**
|
|
37
|
+
* Initialize OpenTelemetry SDK with Azure Monitor exporter
|
|
38
|
+
*
|
|
39
|
+
* Environment variables:
|
|
40
|
+
* - APPLICATIONINSIGHTS_CONNECTION_STRING: Azure Application Insights connection string
|
|
41
|
+
* - SERVICE_NAME: Service name (default: 'unknown-service')
|
|
42
|
+
* - APP_VERSION: Service version (default: 'unknown')
|
|
43
|
+
* - OTEL_SERVICE_NAME: OpenTelemetry service name (falls back to SERVICE_NAME)
|
|
44
|
+
* - OTEL_LOG_LEVEL: OpenTelemetry log level (default: 'info')
|
|
45
|
+
*
|
|
46
|
+
* @returns NodeSDK instance or null if initialization fails
|
|
47
|
+
*/
|
|
48
|
+
function initializeTracing() {
|
|
49
|
+
// Skip if already initialized
|
|
50
|
+
if (sdk) {
|
|
51
|
+
(0, logger_1.logger)().warn({ message: 'OpenTelemetry SDK already initialized' });
|
|
52
|
+
return sdk;
|
|
53
|
+
}
|
|
54
|
+
const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
55
|
+
const serviceName = process.env.OTEL_SERVICE_NAME || process.env.SERVICE_NAME || 'unknown-service';
|
|
56
|
+
const serviceVersion = process.env.APP_VERSION || 'unknown';
|
|
57
|
+
// If no connection string, skip initialization (useful for local dev)
|
|
58
|
+
if (!connectionString) {
|
|
59
|
+
(0, logger_1.logger)().warn({
|
|
60
|
+
message: 'OpenTelemetry initialization skipped: APPLICATIONINSIGHTS_CONNECTION_STRING not set',
|
|
61
|
+
serviceName,
|
|
62
|
+
});
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
// Create Azure Monitor trace exporter
|
|
67
|
+
const traceExporter = new monitor_opentelemetry_exporter_1.AzureMonitorTraceExporter({
|
|
68
|
+
connectionString,
|
|
69
|
+
});
|
|
70
|
+
// Create resource with service metadata
|
|
71
|
+
const resource = (0, resources_1.resourceFromAttributes)({
|
|
72
|
+
[semantic_conventions_1.SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
73
|
+
[semantic_conventions_1.SemanticResourceAttributes.SERVICE_VERSION]: serviceVersion,
|
|
74
|
+
[semantic_conventions_1.SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
|
|
75
|
+
});
|
|
76
|
+
// Initialize SDK with auto-instrumentation
|
|
77
|
+
sdk = new sdk_node_1.NodeSDK({
|
|
78
|
+
resource,
|
|
79
|
+
traceExporter,
|
|
80
|
+
instrumentations: [
|
|
81
|
+
(0, auto_instrumentations_node_1.getNodeAutoInstrumentations)({
|
|
82
|
+
// Enable HTTP instrumentation (Express, etc.)
|
|
83
|
+
'@opentelemetry/instrumentation-http': {
|
|
84
|
+
enabled: true,
|
|
85
|
+
},
|
|
86
|
+
// Enable MongoDB instrumentation
|
|
87
|
+
'@opentelemetry/instrumentation-mongodb': {
|
|
88
|
+
enabled: true,
|
|
89
|
+
},
|
|
90
|
+
// Enable Redis instrumentation (if using ioredis or node-redis)
|
|
91
|
+
'@opentelemetry/instrumentation-redis': {
|
|
92
|
+
enabled: true,
|
|
93
|
+
},
|
|
94
|
+
// Enable Kafka instrumentation
|
|
95
|
+
'@opentelemetry/instrumentation-kafkajs': {
|
|
96
|
+
enabled: true,
|
|
97
|
+
},
|
|
98
|
+
// Disable fs instrumentation (too noisy)
|
|
99
|
+
'@opentelemetry/instrumentation-fs': {
|
|
100
|
+
enabled: false,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
// Start the SDK
|
|
106
|
+
sdk.start();
|
|
107
|
+
(0, logger_1.logger)().info({
|
|
108
|
+
message: 'OpenTelemetry SDK initialized successfully',
|
|
109
|
+
serviceName,
|
|
110
|
+
serviceVersion,
|
|
111
|
+
exporter: 'AzureMonitor',
|
|
112
|
+
});
|
|
113
|
+
return sdk;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
(0, logger_1.logger)().error({
|
|
117
|
+
err: error,
|
|
118
|
+
message: 'Failed to initialize OpenTelemetry SDK',
|
|
119
|
+
serviceName,
|
|
120
|
+
});
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Shutdown OpenTelemetry SDK gracefully
|
|
126
|
+
* Call this during service shutdown
|
|
127
|
+
*/
|
|
128
|
+
function shutdownTracing() {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
if (sdk) {
|
|
131
|
+
try {
|
|
132
|
+
yield sdk.shutdown();
|
|
133
|
+
(0, logger_1.logger)().info({ message: 'OpenTelemetry SDK shut down successfully' });
|
|
134
|
+
sdk = null;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
(0, logger_1.logger)().error({
|
|
138
|
+
err: error,
|
|
139
|
+
message: 'Error shutting down OpenTelemetry SDK',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get the current active span
|
|
147
|
+
* @returns Active span or undefined
|
|
148
|
+
*/
|
|
149
|
+
function getActiveSpan() {
|
|
150
|
+
return api_1.trace.getActiveSpan();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Create a new span for an operation
|
|
154
|
+
* @param name Span name
|
|
155
|
+
* @param fn Function to execute within the span
|
|
156
|
+
* @returns Result of the function
|
|
157
|
+
*/
|
|
158
|
+
function withSpan(name, fn) {
|
|
159
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
const tracer = api_1.trace.getTracer(process.env.SERVICE_NAME || 'unknown-service', process.env.APP_VERSION || 'unknown');
|
|
161
|
+
return tracer.startActiveSpan(name, (span) => __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
try {
|
|
163
|
+
// Add correlation ID to span attributes
|
|
164
|
+
const correlationId = (0, correlation_1.getCorrelationId)();
|
|
165
|
+
if (correlationId) {
|
|
166
|
+
span.setAttribute('correlation.id', correlationId);
|
|
167
|
+
}
|
|
168
|
+
const result = yield fn(span);
|
|
169
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
span.setStatus({
|
|
174
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
175
|
+
message: error.message,
|
|
176
|
+
});
|
|
177
|
+
span.recordException(error);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
span.end();
|
|
182
|
+
}
|
|
183
|
+
}));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Add attributes to the current active span
|
|
188
|
+
* @param attributes Key-value pairs to add
|
|
189
|
+
*/
|
|
190
|
+
function addSpanAttributes(attributes) {
|
|
191
|
+
const span = getActiveSpan();
|
|
192
|
+
if (span) {
|
|
193
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
194
|
+
span.setAttribute(key, value);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Record an exception on the current active span
|
|
200
|
+
* @param error Error to record
|
|
201
|
+
*/
|
|
202
|
+
function recordSpanException(error) {
|
|
203
|
+
const span = getActiveSpan();
|
|
204
|
+
if (span) {
|
|
205
|
+
span.recordException(error);
|
|
206
|
+
span.setStatus({
|
|
207
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
208
|
+
message: error.message,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aichatwar/shared",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.147",
|
|
4
4
|
"main": "./build/index.js",
|
|
5
5
|
"typs": "./build/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -28,9 +28,16 @@
|
|
|
28
28
|
"express-validator": "^7.2.1"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.32",
|
|
32
|
+
"@opentelemetry/api": "^1.8.0",
|
|
33
|
+
"@opentelemetry/auto-instrumentations-node": "^0.69.0",
|
|
34
|
+
"@opentelemetry/resources": "^2.5.1",
|
|
35
|
+
"@opentelemetry/semantic-conventions": "^1.24.0",
|
|
36
|
+
"@opentelemetry/sdk-node": "^0.212.0",
|
|
31
37
|
"jsonwebtoken": "^9.0.2",
|
|
32
38
|
"kafkajs": "^2.2.4",
|
|
33
39
|
"mongoose-update-if-current": "^1.4.0",
|
|
34
|
-
"node-nats-streaming": "^0.3.2"
|
|
40
|
+
"node-nats-streaming": "^0.3.2",
|
|
41
|
+
"pino": "^9.9.0"
|
|
35
42
|
}
|
|
36
43
|
}
|