@breadstone/archipel-platform-telemetry 0.0.12 → 0.0.14
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 +6 -5
- package/package.json +3 -3
- package/src/OtelSdkHolder.d.ts +19 -0
- package/src/OtelSdkHolder.js +49 -0
- package/src/TelemetryModule.js +30 -34
- package/src/bootstrap/OtelInitializer.js +8 -4
- package/src/core/TelemetryFacade.d.ts +2 -1
- package/src/core/TelemetryFacade.js +11 -4
- package/src/index.d.ts +5 -3
- package/src/index.js +8 -5
- package/src/interceptor/OperationTelemetryInterceptor.d.ts +1 -1
- package/src/interceptor/OperationTelemetryInterceptor.js +4 -2
- package/src/metrics/MetricsService.d.ts +20 -0
- package/src/metrics/MetricsService.js +51 -0
- package/src/options/TelemetryOptions.d.ts +4 -0
- package/src/options/TelemetryOptions.js +23 -6
- package/src/rules/TelemetryRuleEngine.d.ts +3 -1
- package/src/rules/TelemetryRuleEngine.js +24 -21
- package/src/tokens.d.ts +0 -1
- package/src/tokens.js +1 -2
- package/src/ShutdownHook.d.ts +0 -6
- package/src/ShutdownHook.js +0 -21
- package/src/metrics/MetricsHelper.d.ts +0 -6
- package/src/metrics/MetricsHelper.js +0 -22
package/README.md
CHANGED
|
@@ -14,11 +14,12 @@ import { TelemetryModule } from '@breadstone/archipel-platform-telemetry';
|
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
- Metrics collection
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
17
|
+
- **Metrics collection**: Counter, histogram, and gauge recording via `TelemetryFacade`
|
|
18
|
+
- **Distributed tracing**: OpenTelemetry SDK integration with automatic span creation
|
|
19
|
+
- **Operation interceptor**: `OperationTelemetryInterceptor` wraps controller methods in telemetry operations (returns `Observable<unknown>`)
|
|
20
|
+
- **Graceful degradation**: Falls back to `NoopTelemetryFacade` when disabled — zero overhead
|
|
21
|
+
- **Health checks**: Integration with health module for observability readiness
|
|
22
|
+
- **Structured logging**: `TelemetryLoggerService` with request correlation IDs
|
|
22
23
|
|
|
23
24
|
## Development
|
|
24
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@breadstone/archipel-platform-telemetry",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "OpenTelemetry integration with tracing, metrics, and graceful no-op fallback for NestJS.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@breadstone/archipel-platform-caching": "0.0.
|
|
32
|
-
"@breadstone/archipel-platform-core": "0.0.
|
|
31
|
+
"@breadstone/archipel-platform-caching": "0.0.14",
|
|
32
|
+
"@breadstone/archipel-platform-core": "0.0.14",
|
|
33
33
|
"@opentelemetry/api": "1.9.1",
|
|
34
34
|
"@opentelemetry/exporter-metrics-otlp-http": "0.215.0",
|
|
35
35
|
"@opentelemetry/exporter-trace-otlp-http": "0.215.0",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OnApplicationShutdown } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Holds a reference to the OpenTelemetry NodeSDK instance and
|
|
4
|
+
* performs a graceful shutdown when the application terminates.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the former closure-based `otelSdkRef` pattern (TEL-1).
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export declare class OtelSdkHolder implements OnApplicationShutdown {
|
|
11
|
+
private readonly _logger;
|
|
12
|
+
private _sdk?;
|
|
13
|
+
/**
|
|
14
|
+
* Stores the SDK reference so it can be shut down later.
|
|
15
|
+
*/
|
|
16
|
+
setSdk(sdk: unknown): void;
|
|
17
|
+
/** @inheritdoc */
|
|
18
|
+
onApplicationShutdown(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// #region Imports
|
|
3
|
+
var OtelSdkHolder_1;
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.OtelSdkHolder = void 0;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const common_1 = require("@nestjs/common");
|
|
8
|
+
// #endregion
|
|
9
|
+
/**
|
|
10
|
+
* Holds a reference to the OpenTelemetry NodeSDK instance and
|
|
11
|
+
* performs a graceful shutdown when the application terminates.
|
|
12
|
+
*
|
|
13
|
+
* Replaces the former closure-based `otelSdkRef` pattern (TEL-1).
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
let OtelSdkHolder = OtelSdkHolder_1 = class OtelSdkHolder {
|
|
18
|
+
constructor() {
|
|
19
|
+
// #region Fields
|
|
20
|
+
this._logger = new common_1.Logger(OtelSdkHolder_1.name);
|
|
21
|
+
// #endregion
|
|
22
|
+
}
|
|
23
|
+
// #endregion
|
|
24
|
+
// #region Methods
|
|
25
|
+
/**
|
|
26
|
+
* Stores the SDK reference so it can be shut down later.
|
|
27
|
+
*/
|
|
28
|
+
setSdk(sdk) {
|
|
29
|
+
this._sdk = sdk;
|
|
30
|
+
}
|
|
31
|
+
/** @inheritdoc */
|
|
32
|
+
async onApplicationShutdown() {
|
|
33
|
+
if (!this._sdk) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await this._sdk.shutdown();
|
|
38
|
+
this._logger.log('OpenTelemetry SDK shut down successfully.');
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this._logger.error('Failed to shut down OpenTelemetry SDK.', error.stack);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.OtelSdkHolder = OtelSdkHolder;
|
|
46
|
+
exports.OtelSdkHolder = OtelSdkHolder = OtelSdkHolder_1 = tslib_1.__decorate([
|
|
47
|
+
(0, common_1.Injectable)()
|
|
48
|
+
], OtelSdkHolder);
|
|
49
|
+
//# sourceMappingURL=OtelSdkHolder.js.map
|
package/src/TelemetryModule.js
CHANGED
|
@@ -5,75 +5,71 @@ exports.TelemetryModule = void 0;
|
|
|
5
5
|
const tslib_1 = require("tslib");
|
|
6
6
|
const common_1 = require("@nestjs/common");
|
|
7
7
|
const core_1 = require("@nestjs/core");
|
|
8
|
-
const TelemetryOptions_1 = require("./options/TelemetryOptions");
|
|
9
|
-
const tokens_1 = require("./tokens");
|
|
10
8
|
const OtelInitializer_1 = require("./bootstrap/OtelInitializer");
|
|
11
9
|
const NoopTelemetryFacade_1 = require("./core/NoopTelemetryFacade");
|
|
12
10
|
const TelemetryFacade_1 = require("./core/TelemetryFacade");
|
|
13
|
-
const TelemetryRuleEngine_1 = require("./rules/TelemetryRuleEngine");
|
|
14
|
-
const TelemetryLoggerService_1 = require("./logger/TelemetryLoggerService");
|
|
15
11
|
const OperationTelemetryInterceptor_1 = require("./interceptor/OperationTelemetryInterceptor");
|
|
16
|
-
const
|
|
12
|
+
const TelemetryLoggerService_1 = require("./logger/TelemetryLoggerService");
|
|
13
|
+
const MetricsService_1 = require("./metrics/MetricsService");
|
|
14
|
+
const TelemetryOptions_1 = require("./options/TelemetryOptions");
|
|
15
|
+
const OtelSdkHolder_1 = require("./OtelSdkHolder");
|
|
16
|
+
const TelemetryRuleEngine_1 = require("./rules/TelemetryRuleEngine");
|
|
17
|
+
const tokens_1 = require("./tokens");
|
|
17
18
|
let TelemetryModule = TelemetryModule_1 = class TelemetryModule {
|
|
18
19
|
static register(options) {
|
|
19
|
-
let otelSdkRef;
|
|
20
20
|
return {
|
|
21
21
|
module: TelemetryModule_1,
|
|
22
22
|
providers: [
|
|
23
23
|
{
|
|
24
24
|
provide: tokens_1.TELEMETRY_OPTIONS,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
useFactory: () => {
|
|
26
|
+
const merged = {
|
|
27
|
+
...TelemetryOptions_1.defaultTelemetryOptions,
|
|
28
|
+
...options,
|
|
29
|
+
};
|
|
30
|
+
(0, TelemetryOptions_1.validateTelemetryOptions)(merged);
|
|
31
|
+
return merged;
|
|
32
|
+
},
|
|
29
33
|
},
|
|
30
34
|
{
|
|
31
35
|
provide: tokens_1.TELEMETRY_ENABLED,
|
|
32
36
|
inject: [tokens_1.TELEMETRY_OPTIONS],
|
|
33
|
-
useFactory: (cfg) => cfg.enabled
|
|
37
|
+
useFactory: (cfg) => cfg.enabled,
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
provide: TelemetryRuleEngine_1.TelemetryRuleEngine,
|
|
37
|
-
inject: [tokens_1.TELEMETRY_OPTIONS],
|
|
38
|
-
useFactory: (cfg) => new TelemetryRuleEngine_1.TelemetryRuleEngine(cfg)
|
|
41
|
+
inject: [tokens_1.TELEMETRY_OPTIONS, MetricsService_1.MetricsService],
|
|
42
|
+
useFactory: (cfg, metricsService) => new TelemetryRuleEngine_1.TelemetryRuleEngine(cfg, metricsService),
|
|
39
43
|
},
|
|
40
44
|
TelemetryLoggerService_1.TelemetryLoggerService,
|
|
41
|
-
|
|
45
|
+
OtelSdkHolder_1.OtelSdkHolder,
|
|
46
|
+
MetricsService_1.MetricsService,
|
|
42
47
|
{
|
|
43
48
|
provide: tokens_1.TELEMETRY_FACADE,
|
|
44
|
-
inject: [tokens_1.TELEMETRY_OPTIONS, tokens_1.TELEMETRY_ENABLED],
|
|
45
|
-
useFactory: async (cfg, enabled) => {
|
|
49
|
+
inject: [tokens_1.TELEMETRY_OPTIONS, tokens_1.TELEMETRY_ENABLED, OtelSdkHolder_1.OtelSdkHolder],
|
|
50
|
+
useFactory: async (cfg, enabled, sdkHolder) => {
|
|
46
51
|
if (!enabled)
|
|
47
52
|
return new NoopTelemetryFacade_1.NoopTelemetryFacade();
|
|
48
53
|
try {
|
|
49
54
|
const { sdk } = await (0, OtelInitializer_1.initializeOtel)(cfg);
|
|
50
|
-
|
|
55
|
+
sdkHolder.setSdk(sdk);
|
|
51
56
|
return new TelemetryFacade_1.TelemetryFacade();
|
|
52
57
|
}
|
|
53
|
-
catch {
|
|
58
|
+
catch (error) {
|
|
59
|
+
new common_1.Logger('TelemetryModule').error('OpenTelemetry SDK initialization failed, falling back to noop', error.stack);
|
|
54
60
|
return new NoopTelemetryFacade_1.NoopTelemetryFacade();
|
|
55
61
|
}
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
provide: tokens_1.TELEMETRY_SHUTDOWN,
|
|
60
|
-
inject: [tokens_1.TELEMETRY_ENABLED],
|
|
61
|
-
useFactory: (enabled) => async () => {
|
|
62
|
-
if (enabled) {
|
|
63
|
-
try {
|
|
64
|
-
await otelSdkRef?.shutdown?.();
|
|
65
|
-
}
|
|
66
|
-
catch { /* noop */ }
|
|
67
|
-
}
|
|
68
|
-
}
|
|
62
|
+
},
|
|
69
63
|
},
|
|
70
64
|
{
|
|
71
65
|
provide: core_1.APP_INTERCEPTOR,
|
|
72
66
|
inject: [tokens_1.TELEMETRY_FACADE, tokens_1.TELEMETRY_OPTIONS],
|
|
73
|
-
useFactory: (facade, cfg) => cfg.interceptor?.operation
|
|
74
|
-
|
|
67
|
+
useFactory: (facade, cfg) => cfg.interceptor?.operation
|
|
68
|
+
? new OperationTelemetryInterceptor_1.OperationTelemetryInterceptor(facade)
|
|
69
|
+
: { intercept: (_, next) => next.handle() },
|
|
70
|
+
},
|
|
75
71
|
],
|
|
76
|
-
exports: [tokens_1.TELEMETRY_FACADE]
|
|
72
|
+
exports: [tokens_1.TELEMETRY_FACADE, MetricsService_1.MetricsService],
|
|
77
73
|
};
|
|
78
74
|
}
|
|
79
75
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.initializeOtel = initializeOtel;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const logger = new common_1.Logger('OtelInitializer');
|
|
4
6
|
// Lazy initialization to avoid module load costs when disabled
|
|
5
7
|
async function initializeOtel(options) {
|
|
6
8
|
if (!options.enabled)
|
|
@@ -28,23 +30,25 @@ async function initializeOtel(options) {
|
|
|
28
30
|
const metricExp = require('@opentelemetry/exporter-metrics-otlp-http');
|
|
29
31
|
metricReader = new sdkNode.PeriodicExportingMetricReader({
|
|
30
32
|
exporter: new metricExp.OTLPMetricExporter(),
|
|
31
|
-
exportIntervalMillis: options.metrics?.intervalMs ?? 60000
|
|
33
|
+
exportIntervalMillis: options.metrics?.intervalMs ?? 60000,
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
const resource = new resources.Resource({
|
|
35
37
|
[semconv.SemanticResourceAttributes.SERVICE_NAME]: options.serviceName,
|
|
36
|
-
environment: options.environment ?? 'local'
|
|
38
|
+
environment: options.environment ?? 'local',
|
|
37
39
|
});
|
|
38
40
|
const sdk = new sdkNode.NodeSDK({
|
|
39
41
|
resource,
|
|
40
42
|
traceExporter,
|
|
41
43
|
metricReader,
|
|
42
|
-
instrumentations
|
|
44
|
+
instrumentations,
|
|
43
45
|
});
|
|
44
46
|
await sdk.start();
|
|
47
|
+
logger.log('OpenTelemetry SDK initialized successfully.');
|
|
45
48
|
return { sdk };
|
|
46
49
|
}
|
|
47
|
-
catch {
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.error('Failed to initialize OpenTelemetry SDK. Telemetry will be degraded.', error.stack);
|
|
48
52
|
return {};
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ITelemetryFacade } from './NoopTelemetryFacade';
|
|
2
1
|
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { ITelemetryFacade } from './NoopTelemetryFacade';
|
|
3
3
|
export declare class TelemetryFacade implements ITelemetryFacade {
|
|
4
4
|
private readonly _logger;
|
|
5
|
+
constructor(logger?: Logger);
|
|
5
6
|
runOperation<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
|
|
6
7
|
recordCacheEvent(event: {
|
|
7
8
|
cache: string;
|
|
@@ -3,9 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TelemetryFacade = void 0;
|
|
4
4
|
const common_1 = require("@nestjs/common");
|
|
5
5
|
class TelemetryFacade {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// #endregion
|
|
7
|
+
// #region Ctor
|
|
8
|
+
constructor(logger) {
|
|
9
|
+
this._logger = logger ?? new common_1.Logger(TelemetryFacade.name);
|
|
8
10
|
}
|
|
11
|
+
// #endregion
|
|
12
|
+
// #region Methods
|
|
9
13
|
async runOperation(name, fn) {
|
|
10
14
|
const start = Date.now();
|
|
11
15
|
try {
|
|
@@ -25,12 +29,15 @@ class TelemetryFacade {
|
|
|
25
29
|
}
|
|
26
30
|
recordClientMetric(name, type, value, labels) {
|
|
27
31
|
const labelStr = labels
|
|
28
|
-
? Object.entries(labels)
|
|
32
|
+
? Object.entries(labels)
|
|
33
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
29
34
|
.join(' ')
|
|
30
35
|
: '';
|
|
31
36
|
this._logger.debug(`client.metric name=${name} type=${type} value=${value}${labelStr ? ' ' + labelStr : ''}`);
|
|
32
37
|
}
|
|
33
|
-
getLogger() {
|
|
38
|
+
getLogger() {
|
|
39
|
+
return this._logger;
|
|
40
|
+
}
|
|
34
41
|
}
|
|
35
42
|
exports.TelemetryFacade = TelemetryFacade;
|
|
36
43
|
//# sourceMappingURL=TelemetryFacade.js.map
|
package/src/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export { TelemetryModule } from './TelemetryModule';
|
|
2
1
|
export * from './core/NoopTelemetryFacade';
|
|
3
|
-
export * from './core/TelemetryFacade';
|
|
4
2
|
export * from './core/TelemetryCacheMetricsRecorder';
|
|
5
|
-
export * from './
|
|
3
|
+
export * from './core/TelemetryFacade';
|
|
6
4
|
export * from './logger/TelemetryLoggerService';
|
|
5
|
+
export * from './metrics/MetricsService';
|
|
7
6
|
export * from './options/TelemetryOptions';
|
|
7
|
+
export { OtelSdkHolder } from './OtelSdkHolder';
|
|
8
|
+
export * from './rules/TelemetryRuleEngine';
|
|
9
|
+
export { TelemetryModule } from './TelemetryModule';
|
|
8
10
|
export * from './tokens';
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TelemetryModule = void 0;
|
|
3
|
+
exports.TelemetryModule = exports.OtelSdkHolder = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
var TelemetryModule_1 = require("./TelemetryModule");
|
|
6
|
-
Object.defineProperty(exports, "TelemetryModule", { enumerable: true, get: function () { return TelemetryModule_1.TelemetryModule; } });
|
|
7
5
|
tslib_1.__exportStar(require("./core/NoopTelemetryFacade"), exports);
|
|
8
|
-
tslib_1.__exportStar(require("./core/TelemetryFacade"), exports);
|
|
9
6
|
tslib_1.__exportStar(require("./core/TelemetryCacheMetricsRecorder"), exports);
|
|
10
|
-
tslib_1.__exportStar(require("./
|
|
7
|
+
tslib_1.__exportStar(require("./core/TelemetryFacade"), exports);
|
|
11
8
|
tslib_1.__exportStar(require("./logger/TelemetryLoggerService"), exports);
|
|
9
|
+
tslib_1.__exportStar(require("./metrics/MetricsService"), exports);
|
|
12
10
|
tslib_1.__exportStar(require("./options/TelemetryOptions"), exports);
|
|
11
|
+
var OtelSdkHolder_1 = require("./OtelSdkHolder");
|
|
12
|
+
Object.defineProperty(exports, "OtelSdkHolder", { enumerable: true, get: function () { return OtelSdkHolder_1.OtelSdkHolder; } });
|
|
13
|
+
tslib_1.__exportStar(require("./rules/TelemetryRuleEngine"), exports);
|
|
14
|
+
var TelemetryModule_1 = require("./TelemetryModule");
|
|
15
|
+
Object.defineProperty(exports, "TelemetryModule", { enumerable: true, get: function () { return TelemetryModule_1.TelemetryModule; } });
|
|
13
16
|
tslib_1.__exportStar(require("./tokens"), exports);
|
|
14
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,5 +4,5 @@ import { ITelemetryFacade } from '../core/NoopTelemetryFacade';
|
|
|
4
4
|
export declare class OperationTelemetryInterceptor implements NestInterceptor {
|
|
5
5
|
private readonly _facade;
|
|
6
6
|
constructor(facade: ITelemetryFacade);
|
|
7
|
-
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown
|
|
7
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
8
8
|
}
|
|
@@ -7,14 +7,16 @@ const rxjs_1 = require("rxjs");
|
|
|
7
7
|
let OperationTelemetryInterceptor = class OperationTelemetryInterceptor {
|
|
8
8
|
// #endregion
|
|
9
9
|
// #region Ctor
|
|
10
|
-
constructor(facade) {
|
|
10
|
+
constructor(facade) {
|
|
11
|
+
this._facade = facade;
|
|
12
|
+
}
|
|
11
13
|
// #endregion
|
|
12
14
|
// #region Methods
|
|
13
15
|
intercept(context, next) {
|
|
14
16
|
const handler = context.getHandler().name;
|
|
15
17
|
const cls = context.getClass().name;
|
|
16
18
|
const name = `${cls}.${handler}`;
|
|
17
|
-
return this._facade.runOperation(name,
|
|
19
|
+
return (0, rxjs_1.from)(this._facade.runOperation(name, () => (0, rxjs_1.firstValueFrom)(next.handle())));
|
|
18
20
|
}
|
|
19
21
|
};
|
|
20
22
|
exports.OperationTelemetryInterceptor = OperationTelemetryInterceptor;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface IMetricInstruments {
|
|
2
|
+
cacheCounter?: unknown;
|
|
3
|
+
opCounter?: unknown;
|
|
4
|
+
opDuration?: unknown;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Provides lazily-initialized OpenTelemetry metric instruments.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the former module-level `let instruments` closure (TEL-4).
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export declare class MetricsService {
|
|
14
|
+
private readonly _logger;
|
|
15
|
+
private _instruments?;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the cached metric instruments, creating them on first access.
|
|
18
|
+
*/
|
|
19
|
+
getInstruments(): IMetricInstruments | undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// #region Imports
|
|
3
|
+
var MetricsService_1;
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.MetricsService = void 0;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const common_1 = require("@nestjs/common");
|
|
8
|
+
// #endregion
|
|
9
|
+
/**
|
|
10
|
+
* Provides lazily-initialized OpenTelemetry metric instruments.
|
|
11
|
+
*
|
|
12
|
+
* Replaces the former module-level `let instruments` closure (TEL-4).
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
let MetricsService = MetricsService_1 = class MetricsService {
|
|
17
|
+
constructor() {
|
|
18
|
+
// #region Fields
|
|
19
|
+
this._logger = new common_1.Logger(MetricsService_1.name);
|
|
20
|
+
// #endregion
|
|
21
|
+
}
|
|
22
|
+
// #endregion
|
|
23
|
+
// #region Methods
|
|
24
|
+
/**
|
|
25
|
+
* Returns the cached metric instruments, creating them on first access.
|
|
26
|
+
*/
|
|
27
|
+
getInstruments() {
|
|
28
|
+
if (this._instruments) {
|
|
29
|
+
return this._instruments;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const api = require('@opentelemetry/api');
|
|
33
|
+
const meter = api.metrics.getMeter('telemetry');
|
|
34
|
+
this._instruments = {
|
|
35
|
+
cacheCounter: meter.createCounter('cache_event_total', { description: 'Cache events (hit/miss/save)' }),
|
|
36
|
+
opCounter: meter.createCounter('operation_event_total', { description: 'Operation success/error events' }),
|
|
37
|
+
opDuration: meter.createHistogram('operation_duration_ms', { description: 'Operation durations ms' }),
|
|
38
|
+
};
|
|
39
|
+
return this._instruments;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
this._logger.warn('Failed to initialize metric instruments.', error.message);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
exports.MetricsService = MetricsService;
|
|
48
|
+
exports.MetricsService = MetricsService = MetricsService_1 = tslib_1.__decorate([
|
|
49
|
+
(0, common_1.Injectable)()
|
|
50
|
+
], MetricsService);
|
|
51
|
+
//# sourceMappingURL=MetricsService.js.map
|
|
@@ -28,3 +28,7 @@ export interface ITelemetryOptions {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
export declare const defaultTelemetryOptions: ITelemetryOptions;
|
|
31
|
+
/**
|
|
32
|
+
* Validates telemetry options and throws on invalid values.
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateTelemetryOptions(options: ITelemetryOptions): void;
|
|
@@ -2,31 +2,48 @@
|
|
|
2
2
|
// #region Interfaces
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.defaultTelemetryOptions = void 0;
|
|
5
|
+
exports.validateTelemetryOptions = validateTelemetryOptions;
|
|
5
6
|
exports.defaultTelemetryOptions = {
|
|
6
7
|
enabled: true,
|
|
7
8
|
serviceName: 'app',
|
|
8
9
|
traces: {
|
|
9
10
|
enabled: true,
|
|
10
11
|
samplingRatio: 0.05,
|
|
11
|
-
exporter: 'otlp-http'
|
|
12
|
+
exporter: 'otlp-http',
|
|
12
13
|
},
|
|
13
14
|
metrics: {
|
|
14
15
|
enabled: true,
|
|
15
16
|
intervalMs: 60000,
|
|
16
|
-
exporter: 'otlp-http'
|
|
17
|
+
exporter: 'otlp-http',
|
|
17
18
|
},
|
|
18
19
|
logCorrelation: true,
|
|
19
20
|
instrumentation: {
|
|
20
21
|
http: true,
|
|
21
|
-
express: true
|
|
22
|
+
express: true,
|
|
22
23
|
},
|
|
23
24
|
pattern: {
|
|
24
25
|
enableCacheMetrics: true,
|
|
25
|
-
enableOperationMetrics: true
|
|
26
|
+
enableOperationMetrics: true,
|
|
26
27
|
},
|
|
27
28
|
interceptor: {
|
|
28
|
-
operation: true
|
|
29
|
-
}
|
|
29
|
+
operation: true,
|
|
30
|
+
},
|
|
30
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* Validates telemetry options and throws on invalid values.
|
|
34
|
+
*/
|
|
35
|
+
function validateTelemetryOptions(options) {
|
|
36
|
+
if (options.enabled && (!options.serviceName || options.serviceName.trim().length === 0)) {
|
|
37
|
+
throw new Error('TelemetryOptions: serviceName must be a non-empty string when telemetry is enabled.');
|
|
38
|
+
}
|
|
39
|
+
const ratio = options.traces?.samplingRatio;
|
|
40
|
+
if (ratio !== undefined && (ratio < 0 || ratio > 1)) {
|
|
41
|
+
throw new Error(`TelemetryOptions: traces.samplingRatio must be between 0 and 1, got ${ratio}.`);
|
|
42
|
+
}
|
|
43
|
+
const interval = options.metrics?.intervalMs;
|
|
44
|
+
if (interval !== undefined && interval <= 0) {
|
|
45
|
+
throw new Error(`TelemetryOptions: metrics.intervalMs must be positive, got ${interval}.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
31
48
|
// #endregion
|
|
32
49
|
//# sourceMappingURL=TelemetryOptions.js.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { MetricsService } from '../metrics/MetricsService';
|
|
1
2
|
import { ITelemetryOptions } from '../options/TelemetryOptions';
|
|
2
3
|
export declare class TelemetryRuleEngine {
|
|
3
4
|
private readonly _options;
|
|
4
|
-
|
|
5
|
+
private readonly _metricsService;
|
|
6
|
+
constructor(options: ITelemetryOptions, metricsService: MetricsService);
|
|
5
7
|
process(message: string): void;
|
|
6
8
|
private handleCache;
|
|
7
9
|
private handleOperation;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TelemetryRuleEngine = void 0;
|
|
4
|
-
const MetricsHelper_1 = require("../metrics/MetricsHelper");
|
|
5
4
|
class TelemetryRuleEngine {
|
|
6
5
|
// #endregion
|
|
7
6
|
// #region Ctor
|
|
8
|
-
constructor(options) {
|
|
7
|
+
constructor(options, metricsService) {
|
|
8
|
+
this._options = options;
|
|
9
|
+
this._metricsService = metricsService;
|
|
10
|
+
}
|
|
9
11
|
// #endregion
|
|
10
12
|
// #region Methods
|
|
11
13
|
process(message) {
|
|
@@ -24,7 +26,7 @@ class TelemetryRuleEngine {
|
|
|
24
26
|
if (!this._options.pattern?.enableCacheMetrics)
|
|
25
27
|
return;
|
|
26
28
|
const tokens = this.parseTokens(line);
|
|
27
|
-
const instruments =
|
|
29
|
+
const instruments = this._metricsService.getInstruments();
|
|
28
30
|
const counter = instruments?.cacheCounter;
|
|
29
31
|
if (!counter)
|
|
30
32
|
return;
|
|
@@ -32,67 +34,68 @@ class TelemetryRuleEngine {
|
|
|
32
34
|
counter.add(1, {
|
|
33
35
|
cache: tokens['cache'] ?? 'unknown',
|
|
34
36
|
outcome: tokens['outcome'] ?? 'unknown',
|
|
35
|
-
phase: tokens['phase'] ?? 'none'
|
|
37
|
+
phase: tokens['phase'] ?? 'none',
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
|
-
catch {
|
|
40
|
+
catch {
|
|
41
|
+
/* noop */
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
handleOperation(line) {
|
|
41
45
|
if (!this._options.pattern?.enableOperationMetrics)
|
|
42
46
|
return;
|
|
43
47
|
const success = line.startsWith('op.success');
|
|
44
48
|
const tokens = this.parseTokens(line);
|
|
45
|
-
const instruments =
|
|
49
|
+
const instruments = this._metricsService.getInstruments();
|
|
46
50
|
if (!instruments)
|
|
47
51
|
return;
|
|
48
52
|
const counter = instruments.opCounter;
|
|
49
53
|
const histogram = instruments.opDuration;
|
|
50
54
|
const labels = {
|
|
51
55
|
name: tokens['name'] ?? 'unknown',
|
|
52
|
-
status: success ? 'success' : 'error'
|
|
56
|
+
status: success ? 'success' : 'error',
|
|
53
57
|
};
|
|
54
58
|
try {
|
|
55
59
|
counter?.add(1, labels);
|
|
56
60
|
}
|
|
57
|
-
catch {
|
|
61
|
+
catch {
|
|
62
|
+
/* noop */
|
|
63
|
+
}
|
|
58
64
|
const dur = tokens['duration_ms'] ? Number(tokens['duration_ms']) : undefined;
|
|
59
65
|
if (dur !== undefined && !Number.isNaN(dur)) {
|
|
60
66
|
try {
|
|
61
67
|
histogram?.record(dur, labels);
|
|
62
68
|
}
|
|
63
|
-
catch {
|
|
69
|
+
catch {
|
|
70
|
+
/* noop */
|
|
71
|
+
}
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
handleClientMetric(line) {
|
|
67
|
-
// Always enabled if arrives; client submission is already filtered front-side.
|
|
68
75
|
const tokens = this.parseTokens(line);
|
|
69
|
-
const instruments =
|
|
76
|
+
const instruments = this._metricsService.getInstruments();
|
|
70
77
|
if (!instruments)
|
|
71
78
|
return;
|
|
72
|
-
const counter = instruments.opCounter;
|
|
79
|
+
const counter = instruments.opCounter;
|
|
73
80
|
const histogram = instruments.opDuration;
|
|
74
81
|
const name = tokens['name'] ?? 'unknown';
|
|
75
82
|
const valueRaw = tokens['value'] ? Number(tokens['value']) : undefined;
|
|
76
83
|
const type = tokens['type'] ?? 'counter';
|
|
77
|
-
const baseLabels = {
|
|
78
|
-
source: 'client',
|
|
79
|
-
name
|
|
80
|
-
};
|
|
84
|
+
const baseLabels = { source: 'client', name };
|
|
81
85
|
try {
|
|
82
86
|
if (type === 'counter') {
|
|
83
87
|
counter?.add(1, baseLabels);
|
|
84
88
|
}
|
|
85
89
|
else if (type === 'gauge' && valueRaw !== undefined && !Number.isNaN(valueRaw)) {
|
|
86
|
-
histogram?.record(valueRaw, {
|
|
87
|
-
...baseLabels,
|
|
88
|
-
gauge: true
|
|
89
|
-
});
|
|
90
|
+
histogram?.record(valueRaw, { ...baseLabels, gauge: true });
|
|
90
91
|
}
|
|
91
92
|
else if (type === 'histogram' && valueRaw !== undefined && !Number.isNaN(valueRaw)) {
|
|
92
93
|
histogram?.record(valueRaw, baseLabels);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
|
-
catch {
|
|
96
|
+
catch {
|
|
97
|
+
/* noop */
|
|
98
|
+
}
|
|
96
99
|
}
|
|
97
100
|
parseTokens(line) {
|
|
98
101
|
const parts = line.split(/\s+/).slice(1);
|
package/src/tokens.d.ts
CHANGED
package/src/tokens.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.TELEMETRY_ENABLED = exports.TELEMETRY_FACADE = exports.TELEMETRY_OPTIONS = void 0;
|
|
4
4
|
// Internal DI tokens (not exposing OTel types)
|
|
5
5
|
exports.TELEMETRY_OPTIONS = Symbol('TELEMETRY_OPTIONS');
|
|
6
6
|
exports.TELEMETRY_FACADE = Symbol('TELEMETRY_FACADE');
|
|
7
7
|
exports.TELEMETRY_ENABLED = Symbol('TELEMETRY_ENABLED');
|
|
8
|
-
exports.TELEMETRY_SHUTDOWN = Symbol('TELEMETRY_SHUTDOWN');
|
|
9
8
|
//# sourceMappingURL=tokens.js.map
|
package/src/ShutdownHook.d.ts
DELETED
package/src/ShutdownHook.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TelemetryShutdownHook = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const common_1 = require("@nestjs/common");
|
|
6
|
-
const tokens_1 = require("./tokens");
|
|
7
|
-
let TelemetryShutdownHook = class TelemetryShutdownHook {
|
|
8
|
-
// #endregion
|
|
9
|
-
// #region Ctor
|
|
10
|
-
constructor(shutdown) { this._shutdown = shutdown; }
|
|
11
|
-
// #endregion
|
|
12
|
-
// #region Methods
|
|
13
|
-
async onApplicationShutdown() { await this._shutdown(); }
|
|
14
|
-
};
|
|
15
|
-
exports.TelemetryShutdownHook = TelemetryShutdownHook;
|
|
16
|
-
exports.TelemetryShutdownHook = TelemetryShutdownHook = tslib_1.__decorate([
|
|
17
|
-
(0, common_1.Injectable)(),
|
|
18
|
-
tslib_1.__param(0, (0, common_1.Inject)(tokens_1.TELEMETRY_SHUTDOWN)),
|
|
19
|
-
tslib_1.__metadata("design:paramtypes", [Function])
|
|
20
|
-
], TelemetryShutdownHook);
|
|
21
|
-
//# sourceMappingURL=ShutdownHook.js.map
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getInstruments = getInstruments;
|
|
4
|
-
let instruments;
|
|
5
|
-
function getInstruments() {
|
|
6
|
-
if (instruments)
|
|
7
|
-
return instruments;
|
|
8
|
-
try {
|
|
9
|
-
const api = require('@opentelemetry/api');
|
|
10
|
-
const meter = api.metrics.getMeter('telemetry');
|
|
11
|
-
instruments = {
|
|
12
|
-
cacheCounter: meter.createCounter('cache_event_total', { description: 'Cache events (hit/miss/save)' }),
|
|
13
|
-
opCounter: meter.createCounter('operation_event_total', { description: 'Operation success/error events' }),
|
|
14
|
-
opDuration: meter.createHistogram('operation_duration_ms', { description: 'Operation durations ms' })
|
|
15
|
-
};
|
|
16
|
-
return instruments;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=MetricsHelper.js.map
|