@piramesse/otel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +20 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +23 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +206 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/tracer.d.ts +30 -0
- package/dist/core/tracer.d.ts.map +1 -0
- package/dist/core/tracer.js +173 -0
- package/dist/core/tracer.js.map +1 -0
- package/dist/core/types.d.ts +32 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/index.d.ts +4 -0
- package/dist/nest/index.d.ts.map +1 -0
- package/dist/nest/index.js +20 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/otel-logger.service.d.ts +9 -0
- package/dist/nest/otel-logger.service.d.ts.map +1 -0
- package/dist/nest/otel-logger.service.js +32 -0
- package/dist/nest/otel-logger.service.js.map +1 -0
- package/dist/nest/otel-tracer.service.d.ts +10 -0
- package/dist/nest/otel-tracer.service.d.ts.map +1 -0
- package/dist/nest/otel-tracer.service.js +33 -0
- package/dist/nest/otel-tracer.service.js.map +1 -0
- package/dist/nest/otel.module.d.ts +3 -0
- package/dist/nest/otel.module.d.ts.map +1 -0
- package/dist/nest/otel.module.js +23 -0
- package/dist/nest/otel.module.js.map +1 -0
- package/package.json +32 -0
- package/src/core/index.ts +3 -0
- package/src/core/logger.ts +241 -0
- package/src/core/tracer.ts +213 -0
- package/src/core/types.ts +40 -0
- package/src/index.ts +5 -0
- package/src/nest/index.ts +3 -0
- package/src/nest/otel-logger.service.ts +16 -0
- package/src/nest/otel-tracer.service.ts +20 -0
- package/src/nest/otel.module.ts +10 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OtelLoggerService = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const config_1 = require("@nestjs/config");
|
|
15
|
+
const logger_1 = require("../core/logger");
|
|
16
|
+
/**
|
|
17
|
+
* NestJS injectable wrapper for OtelLogger
|
|
18
|
+
*/
|
|
19
|
+
let OtelLoggerService = class OtelLoggerService extends logger_1.OtelLogger {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
super({
|
|
22
|
+
endpoint: config.get('OTEL_EXPORTER_OTLP_ENDPOINT', ''),
|
|
23
|
+
serviceName: config.get('OTEL_SERVICE_NAME', 'unknown-service'),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
exports.OtelLoggerService = OtelLoggerService;
|
|
28
|
+
exports.OtelLoggerService = OtelLoggerService = __decorate([
|
|
29
|
+
(0, common_1.Injectable)(),
|
|
30
|
+
__metadata("design:paramtypes", [config_1.ConfigService])
|
|
31
|
+
], OtelLoggerService);
|
|
32
|
+
//# sourceMappingURL=otel-logger.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel-logger.service.js","sourceRoot":"","sources":["../../src/nest/otel-logger.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,2CAA+C;AAC/C,2CAA4C;AAE5C;;GAEG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,mBAAU;IAC/C,YAAY,MAAqB;QAC/B,KAAK,CAAC;YACJ,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,6BAA6B,EAAE,EAAE,CAAC;YACvD,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,EAAE,iBAAiB,CAAC;SAChE,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAPY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAES,sBAAa;GADtB,iBAAiB,CAO7B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ConfigService } from '@nestjs/config';
|
|
2
|
+
import { OtelTracer } from '../core/tracer';
|
|
3
|
+
import { OtelLoggerService } from './otel-logger.service';
|
|
4
|
+
/**
|
|
5
|
+
* NestJS injectable wrapper for OtelTracer
|
|
6
|
+
*/
|
|
7
|
+
export declare class OtelTracerService extends OtelTracer {
|
|
8
|
+
constructor(config: ConfigService, logger: OtelLoggerService);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=otel-tracer.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel-tracer.service.d.ts","sourceRoot":"","sources":["../../src/nest/otel-tracer.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;GAEG;AACH,qBACa,iBAAkB,SAAQ,UAAU;gBACnC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB;CAS7D"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OtelTracerService = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const config_1 = require("@nestjs/config");
|
|
15
|
+
const tracer_1 = require("../core/tracer");
|
|
16
|
+
const otel_logger_service_1 = require("./otel-logger.service");
|
|
17
|
+
/**
|
|
18
|
+
* NestJS injectable wrapper for OtelTracer
|
|
19
|
+
*/
|
|
20
|
+
let OtelTracerService = class OtelTracerService extends tracer_1.OtelTracer {
|
|
21
|
+
constructor(config, logger) {
|
|
22
|
+
super({
|
|
23
|
+
endpoint: config.get('OTEL_EXPORTER_OTLP_ENDPOINT', ''),
|
|
24
|
+
serviceName: config.get('OTEL_SERVICE_NAME', 'unknown-service'),
|
|
25
|
+
}, logger);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.OtelTracerService = OtelTracerService;
|
|
29
|
+
exports.OtelTracerService = OtelTracerService = __decorate([
|
|
30
|
+
(0, common_1.Injectable)(),
|
|
31
|
+
__metadata("design:paramtypes", [config_1.ConfigService, otel_logger_service_1.OtelLoggerService])
|
|
32
|
+
], OtelTracerService);
|
|
33
|
+
//# sourceMappingURL=otel-tracer.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel-tracer.service.js","sourceRoot":"","sources":["../../src/nest/otel-tracer.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,2CAA+C;AAC/C,2CAA4C;AAC5C,+DAA0D;AAE1D;;GAEG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,mBAAU;IAC/C,YAAY,MAAqB,EAAE,MAAyB;QAC1D,KAAK,CACH;YACE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,6BAA6B,EAAE,EAAE,CAAC;YACvD,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,EAAE,iBAAiB,CAAC;SAChE,EACD,MAAM,CACP,CAAC;IACJ,CAAC;CACF,CAAA;AAVY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAES,sBAAa,EAAU,uCAAiB;GADjD,iBAAiB,CAU7B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.module.d.ts","sourceRoot":"","sources":["../../src/nest/otel.module.ts"],"names":[],"mappings":"AAIA,qBAKa,UAAU;CAAG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.OtelModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const otel_logger_service_1 = require("./otel-logger.service");
|
|
12
|
+
const otel_tracer_service_1 = require("./otel-tracer.service");
|
|
13
|
+
let OtelModule = class OtelModule {
|
|
14
|
+
};
|
|
15
|
+
exports.OtelModule = OtelModule;
|
|
16
|
+
exports.OtelModule = OtelModule = __decorate([
|
|
17
|
+
(0, common_1.Global)(),
|
|
18
|
+
(0, common_1.Module)({
|
|
19
|
+
providers: [otel_logger_service_1.OtelLoggerService, otel_tracer_service_1.OtelTracerService],
|
|
20
|
+
exports: [otel_logger_service_1.OtelLoggerService, otel_tracer_service_1.OtelTracerService],
|
|
21
|
+
})
|
|
22
|
+
], OtelModule);
|
|
23
|
+
//# sourceMappingURL=otel.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.module.js","sourceRoot":"","sources":["../../src/nest/otel.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAgD;AAChD,+DAA0D;AAC1D,+DAA0D;AAOnD,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IALtB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,SAAS,EAAE,CAAC,uCAAiB,EAAE,uCAAiB,CAAC;QACjD,OAAO,EAAE,CAAC,uCAAiB,EAAE,uCAAiB,CAAC;KAChD,CAAC;GACW,UAAU,CAAG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@piramesse/otel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenTelemetry logging and tracing for NestJS services",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
13
|
+
"@nestjs/config": "^3.0.0 || ^4.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@nestjs/common": "^11.0.20",
|
|
17
|
+
"@nestjs/config": "^4.0.2",
|
|
18
|
+
"@types/node": "^25.2.0",
|
|
19
|
+
"typescript": "^5.7.3"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OtelConfig,
|
|
3
|
+
LogData,
|
|
4
|
+
ScopedLogger,
|
|
5
|
+
TracedLogger,
|
|
6
|
+
ComponentLogger,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
export class OtelLogger {
|
|
10
|
+
private readonly collectorUrl: string;
|
|
11
|
+
private readonly serviceName: string;
|
|
12
|
+
|
|
13
|
+
constructor(config: OtelConfig) {
|
|
14
|
+
this.collectorUrl = config.endpoint ? `${config.endpoint}/v1/logs` : '';
|
|
15
|
+
this.serviceName = config.serviceName || 'unknown-service';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private getSeverityNumber(level: string): number {
|
|
19
|
+
const severityMap: Record<string, number> = {
|
|
20
|
+
debug: 5,
|
|
21
|
+
info: 9,
|
|
22
|
+
warn: 13,
|
|
23
|
+
error: 17,
|
|
24
|
+
};
|
|
25
|
+
return severityMap[level] || 9;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private convertAttributes(
|
|
29
|
+
attrs: Record<string, any>,
|
|
30
|
+
): Array<{ key: string; value: { stringValue: string } }> {
|
|
31
|
+
return Object.entries(attrs).map(([key, value]) => ({
|
|
32
|
+
key,
|
|
33
|
+
value: { stringValue: String(value) },
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async sendLogToCollector(data: LogData): Promise<void> {
|
|
38
|
+
if (!this.collectorUrl) return;
|
|
39
|
+
|
|
40
|
+
const allAttributes = {
|
|
41
|
+
...(data.component ? { component: data.component } : {}),
|
|
42
|
+
...data.attributes,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const payload = {
|
|
46
|
+
resourceLogs: [
|
|
47
|
+
{
|
|
48
|
+
resource: {
|
|
49
|
+
attributes: [
|
|
50
|
+
{ key: 'service.name', value: { stringValue: this.serviceName } },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
scopeLogs: [
|
|
54
|
+
{
|
|
55
|
+
scope: {
|
|
56
|
+
name: data.component || 'app-logger',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
},
|
|
59
|
+
logRecords: [
|
|
60
|
+
{
|
|
61
|
+
timeUnixNano: String(Date.now() * 1000000),
|
|
62
|
+
observedTimeUnixNano: String(Date.now() * 1000000),
|
|
63
|
+
severityNumber: this.getSeverityNumber(data.level),
|
|
64
|
+
severityText: data.level.toUpperCase(),
|
|
65
|
+
body: { stringValue: data.message },
|
|
66
|
+
attributes: this.convertAttributes(allAttributes),
|
|
67
|
+
...(data.traceId && data.spanId
|
|
68
|
+
? { traceId: data.traceId, spanId: data.spanId, flags: 1 }
|
|
69
|
+
: {}),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await fetch(this.collectorUrl, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(payload),
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
// Silently fail
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
info(msg: string, attrs?: Record<string, any>): void {
|
|
90
|
+
void this.sendLogToCollector({
|
|
91
|
+
message: msg,
|
|
92
|
+
level: 'info',
|
|
93
|
+
attributes: attrs,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
warn(msg: string, attrs?: Record<string, any>): void {
|
|
98
|
+
void this.sendLogToCollector({
|
|
99
|
+
message: msg,
|
|
100
|
+
level: 'warn',
|
|
101
|
+
attributes: attrs,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
error(
|
|
106
|
+
msg: string,
|
|
107
|
+
error?: Error | Record<string, any>,
|
|
108
|
+
attrs?: Record<string, any>,
|
|
109
|
+
): void {
|
|
110
|
+
const errorAttrs =
|
|
111
|
+
error instanceof Error
|
|
112
|
+
? { error: error.message, stack: error.stack, ...attrs }
|
|
113
|
+
: { ...error, ...attrs };
|
|
114
|
+
|
|
115
|
+
void this.sendLogToCollector({
|
|
116
|
+
message: msg,
|
|
117
|
+
level: 'error',
|
|
118
|
+
attributes: errorAttrs,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
debug(msg: string, attrs?: Record<string, any>): void {
|
|
123
|
+
void this.sendLogToCollector({
|
|
124
|
+
message: msg,
|
|
125
|
+
level: 'debug',
|
|
126
|
+
attributes: attrs,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
withTrace(traceId: string, spanId: string, component?: string): ScopedLogger {
|
|
131
|
+
return {
|
|
132
|
+
info: (msg: string, attrs?: Record<string, any>) => {
|
|
133
|
+
void this.sendLogToCollector({
|
|
134
|
+
message: msg,
|
|
135
|
+
level: 'info',
|
|
136
|
+
attributes: attrs,
|
|
137
|
+
traceId,
|
|
138
|
+
spanId,
|
|
139
|
+
component,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
warn: (msg: string, attrs?: Record<string, any>) => {
|
|
143
|
+
void this.sendLogToCollector({
|
|
144
|
+
message: msg,
|
|
145
|
+
level: 'warn',
|
|
146
|
+
attributes: attrs,
|
|
147
|
+
traceId,
|
|
148
|
+
spanId,
|
|
149
|
+
component,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
error: (
|
|
153
|
+
msg: string,
|
|
154
|
+
error?: Error | Record<string, any>,
|
|
155
|
+
attrs?: Record<string, any>,
|
|
156
|
+
) => {
|
|
157
|
+
const errorAttrs =
|
|
158
|
+
error instanceof Error
|
|
159
|
+
? { error: error.message, stack: error.stack, ...attrs }
|
|
160
|
+
: { ...error, ...attrs };
|
|
161
|
+
|
|
162
|
+
void this.sendLogToCollector({
|
|
163
|
+
message: msg,
|
|
164
|
+
level: 'error',
|
|
165
|
+
attributes: errorAttrs,
|
|
166
|
+
traceId,
|
|
167
|
+
spanId,
|
|
168
|
+
component,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
debug: (msg: string, attrs?: Record<string, any>) => {
|
|
172
|
+
void this.sendLogToCollector({
|
|
173
|
+
message: msg,
|
|
174
|
+
level: 'debug',
|
|
175
|
+
attributes: attrs,
|
|
176
|
+
traceId,
|
|
177
|
+
spanId,
|
|
178
|
+
component,
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a scoped logger for a specific component
|
|
186
|
+
*/
|
|
187
|
+
forComponent(component: string): ComponentLogger {
|
|
188
|
+
return {
|
|
189
|
+
info: (msg: string, attrs?: Record<string, any>) => {
|
|
190
|
+
void this.sendLogToCollector({
|
|
191
|
+
message: msg,
|
|
192
|
+
level: 'info',
|
|
193
|
+
attributes: attrs,
|
|
194
|
+
component,
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
warn: (msg: string, attrs?: Record<string, any>) => {
|
|
198
|
+
void this.sendLogToCollector({
|
|
199
|
+
message: msg,
|
|
200
|
+
level: 'warn',
|
|
201
|
+
attributes: attrs,
|
|
202
|
+
component,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
error: (
|
|
206
|
+
msg: string,
|
|
207
|
+
error?: Error | Record<string, any>,
|
|
208
|
+
attrs?: Record<string, any>,
|
|
209
|
+
) => {
|
|
210
|
+
const errorAttrs =
|
|
211
|
+
error instanceof Error
|
|
212
|
+
? { error: error.message, stack: error.stack, ...attrs }
|
|
213
|
+
: { ...error, ...attrs };
|
|
214
|
+
|
|
215
|
+
void this.sendLogToCollector({
|
|
216
|
+
message: msg,
|
|
217
|
+
level: 'error',
|
|
218
|
+
attributes: errorAttrs,
|
|
219
|
+
component,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
debug: (msg: string, attrs?: Record<string, any>) => {
|
|
223
|
+
void this.sendLogToCollector({
|
|
224
|
+
message: msg,
|
|
225
|
+
level: 'debug',
|
|
226
|
+
attributes: attrs,
|
|
227
|
+
component,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
withTrace: (traceId: string, spanId: string) =>
|
|
231
|
+
this.withTrace(traceId, spanId, component),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Factory function to create a logger instance
|
|
238
|
+
*/
|
|
239
|
+
export function createOtelLogger(config: OtelConfig): OtelLogger {
|
|
240
|
+
return new OtelLogger(config);
|
|
241
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { OtelConfig, SpanContext } from './types';
|
|
3
|
+
import { OtelLogger } from './logger';
|
|
4
|
+
|
|
5
|
+
interface SpanData {
|
|
6
|
+
traceId: string;
|
|
7
|
+
spanId: string;
|
|
8
|
+
parentSpanId?: string;
|
|
9
|
+
name: string;
|
|
10
|
+
kind: number;
|
|
11
|
+
startTimeUnixNano: string;
|
|
12
|
+
endTimeUnixNano: string;
|
|
13
|
+
attributes: Array<{ key: string; value: any }>;
|
|
14
|
+
events: Array<{ name: string; timeUnixNano: string; attributes?: any[] }>;
|
|
15
|
+
status: { code: number; message?: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class OtelTracer {
|
|
19
|
+
private readonly collectorUrl: string;
|
|
20
|
+
private readonly serviceName: string;
|
|
21
|
+
private readonly logger: OtelLogger;
|
|
22
|
+
private component?: string;
|
|
23
|
+
|
|
24
|
+
constructor(config: OtelConfig, logger?: OtelLogger) {
|
|
25
|
+
this.collectorUrl = config.endpoint ? `${config.endpoint}/v1/traces` : '';
|
|
26
|
+
this.serviceName = config.serviceName || 'unknown-service';
|
|
27
|
+
this.logger = logger || new OtelLogger(config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a component-scoped tracer
|
|
32
|
+
*/
|
|
33
|
+
forComponent(component: string): OtelTracer {
|
|
34
|
+
const scoped = Object.create(this) as OtelTracer;
|
|
35
|
+
scoped.component = component;
|
|
36
|
+
return scoped;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private generateTraceId(): string {
|
|
40
|
+
return randomBytes(16).toString('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private generateSpanId(): string {
|
|
44
|
+
return randomBytes(8).toString('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private convertAttributes(
|
|
48
|
+
attrs: Record<string, any>,
|
|
49
|
+
): Array<{ key: string; value: any }> {
|
|
50
|
+
return Object.entries(attrs).map(([key, value]) => {
|
|
51
|
+
if (typeof value === 'string') {
|
|
52
|
+
return { key, value: { stringValue: value } };
|
|
53
|
+
} else if (typeof value === 'number') {
|
|
54
|
+
return { key, value: { intValue: value } };
|
|
55
|
+
} else if (typeof value === 'boolean') {
|
|
56
|
+
return { key, value: { boolValue: value } };
|
|
57
|
+
} else {
|
|
58
|
+
return { key, value: { stringValue: String(value) } };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async sendTraceToCollector(spans: SpanData[]): Promise<void> {
|
|
64
|
+
if (!this.collectorUrl) return;
|
|
65
|
+
|
|
66
|
+
const payload = {
|
|
67
|
+
resourceSpans: [
|
|
68
|
+
{
|
|
69
|
+
resource: {
|
|
70
|
+
attributes: [
|
|
71
|
+
{ key: 'service.name', value: { stringValue: this.serviceName } },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
scopeSpans: [
|
|
75
|
+
{
|
|
76
|
+
scope: { name: 'app-tracer', version: '1.0.0' },
|
|
77
|
+
spans: spans.map((span) => ({
|
|
78
|
+
traceId: span.traceId,
|
|
79
|
+
spanId: span.spanId,
|
|
80
|
+
...(span.parentSpanId
|
|
81
|
+
? { parentSpanId: span.parentSpanId }
|
|
82
|
+
: {}),
|
|
83
|
+
name: span.name,
|
|
84
|
+
kind: span.kind,
|
|
85
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
86
|
+
endTimeUnixNano: span.endTimeUnixNano,
|
|
87
|
+
attributes: span.attributes,
|
|
88
|
+
events: span.events,
|
|
89
|
+
status: span.status,
|
|
90
|
+
})),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await fetch(this.collectorUrl, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify(payload),
|
|
102
|
+
});
|
|
103
|
+
} catch {
|
|
104
|
+
// Silently fail
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Trace a function execution
|
|
110
|
+
*/
|
|
111
|
+
async trace<T>(
|
|
112
|
+
name: string,
|
|
113
|
+
fn: (ctx: SpanContext) => Promise<T>,
|
|
114
|
+
parentTraceId?: string,
|
|
115
|
+
parentSpanId?: string,
|
|
116
|
+
): Promise<T> {
|
|
117
|
+
const traceId = parentTraceId || this.generateTraceId();
|
|
118
|
+
const spanId = this.generateSpanId();
|
|
119
|
+
const startTime = Date.now();
|
|
120
|
+
|
|
121
|
+
const attributes: Record<string, any> = {};
|
|
122
|
+
const events: Array<{
|
|
123
|
+
name: string;
|
|
124
|
+
timeUnixNano: string;
|
|
125
|
+
attributes?: any[];
|
|
126
|
+
}> = [];
|
|
127
|
+
|
|
128
|
+
const ctx: SpanContext = {
|
|
129
|
+
traceId,
|
|
130
|
+
spanId,
|
|
131
|
+
log: this.logger.withTrace(traceId, spanId, this.component),
|
|
132
|
+
setAttribute: (key: string, value: string | number | boolean) => {
|
|
133
|
+
attributes[key] = value;
|
|
134
|
+
},
|
|
135
|
+
addEvent: (eventName: string, attrs?: Record<string, any>) => {
|
|
136
|
+
events.push({
|
|
137
|
+
name: eventName,
|
|
138
|
+
timeUnixNano: String(Date.now() * 1000000),
|
|
139
|
+
attributes: attrs
|
|
140
|
+
? Object.entries(attrs).map(([key, value]) => ({
|
|
141
|
+
key,
|
|
142
|
+
value: { stringValue: String(value) },
|
|
143
|
+
}))
|
|
144
|
+
: [],
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const result = await fn(ctx);
|
|
151
|
+
|
|
152
|
+
const spanData: SpanData = {
|
|
153
|
+
traceId,
|
|
154
|
+
spanId,
|
|
155
|
+
...(parentSpanId ? { parentSpanId } : {}),
|
|
156
|
+
name,
|
|
157
|
+
kind: 1,
|
|
158
|
+
startTimeUnixNano: String(startTime * 1000000),
|
|
159
|
+
endTimeUnixNano: String(Date.now() * 1000000),
|
|
160
|
+
attributes: this.convertAttributes(attributes),
|
|
161
|
+
events,
|
|
162
|
+
status: { code: 1 },
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
await this.sendTraceToCollector([spanData]);
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
attributes['error'] = true;
|
|
170
|
+
attributes['error.message'] = (error as Error).message;
|
|
171
|
+
if ((error as Error).stack) {
|
|
172
|
+
attributes['error.stack'] = (error as Error).stack;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const spanData: SpanData = {
|
|
176
|
+
traceId,
|
|
177
|
+
spanId,
|
|
178
|
+
...(parentSpanId ? { parentSpanId } : {}),
|
|
179
|
+
name,
|
|
180
|
+
kind: 1,
|
|
181
|
+
startTimeUnixNano: String(startTime * 1000000),
|
|
182
|
+
endTimeUnixNano: String(Date.now() * 1000000),
|
|
183
|
+
attributes: this.convertAttributes(attributes),
|
|
184
|
+
events,
|
|
185
|
+
status: { code: 2, message: (error as Error).message },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
await this.sendTraceToCollector([spanData]);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a child span within an existing trace
|
|
195
|
+
*/
|
|
196
|
+
async childSpan<T>(
|
|
197
|
+
name: string,
|
|
198
|
+
fn: (ctx: SpanContext) => Promise<T>,
|
|
199
|
+
parentCtx: SpanContext,
|
|
200
|
+
): Promise<T> {
|
|
201
|
+
return this.trace(name, fn, parentCtx.traceId, parentCtx.spanId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Factory function to create a tracer instance
|
|
207
|
+
*/
|
|
208
|
+
export function createOtelTracer(
|
|
209
|
+
config: OtelConfig,
|
|
210
|
+
logger?: OtelLogger,
|
|
211
|
+
): OtelTracer {
|
|
212
|
+
return new OtelTracer(config, logger);
|
|
213
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface OtelConfig {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
serviceName: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface LogData {
|
|
7
|
+
message: string;
|
|
8
|
+
level: 'info' | 'warn' | 'error' | 'debug';
|
|
9
|
+
attributes?: Record<string, any>;
|
|
10
|
+
traceId?: string;
|
|
11
|
+
spanId?: string;
|
|
12
|
+
component?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SpanContext {
|
|
16
|
+
traceId: string;
|
|
17
|
+
spanId: string;
|
|
18
|
+
log: ScopedLogger;
|
|
19
|
+
setAttribute: (key: string, value: string | number | boolean) => void;
|
|
20
|
+
addEvent: (name: string, attrs?: Record<string, any>) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ScopedLogger {
|
|
24
|
+
info: (msg: string, attrs?: Record<string, any>) => void;
|
|
25
|
+
warn: (msg: string, attrs?: Record<string, any>) => void;
|
|
26
|
+
error: (
|
|
27
|
+
msg: string,
|
|
28
|
+
error?: Error | Record<string, any>,
|
|
29
|
+
attrs?: Record<string, any>,
|
|
30
|
+
) => void;
|
|
31
|
+
debug: (msg: string, attrs?: Record<string, any>) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TracedLogger extends ScopedLogger {
|
|
35
|
+
withTrace: (traceId: string, spanId: string) => ScopedLogger;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ComponentLogger extends ScopedLogger {
|
|
39
|
+
withTrace: (traceId: string, spanId: string) => ScopedLogger;
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { OtelLogger } from '../core/logger';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NestJS injectable wrapper for OtelLogger
|
|
7
|
+
*/
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class OtelLoggerService extends OtelLogger {
|
|
10
|
+
constructor(config: ConfigService) {
|
|
11
|
+
super({
|
|
12
|
+
endpoint: config.get('OTEL_EXPORTER_OTLP_ENDPOINT', ''),
|
|
13
|
+
serviceName: config.get('OTEL_SERVICE_NAME', 'unknown-service'),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|