@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.
Files changed (46) hide show
  1. package/dist/core/index.d.ts +4 -0
  2. package/dist/core/index.d.ts.map +1 -0
  3. package/dist/core/index.js +20 -0
  4. package/dist/core/index.js.map +1 -0
  5. package/dist/core/logger.d.ts +23 -0
  6. package/dist/core/logger.d.ts.map +1 -0
  7. package/dist/core/logger.js +206 -0
  8. package/dist/core/logger.js.map +1 -0
  9. package/dist/core/tracer.d.ts +30 -0
  10. package/dist/core/tracer.d.ts.map +1 -0
  11. package/dist/core/tracer.js +173 -0
  12. package/dist/core/tracer.js.map +1 -0
  13. package/dist/core/types.d.ts +32 -0
  14. package/dist/core/types.d.ts.map +1 -0
  15. package/dist/core/types.js +3 -0
  16. package/dist/core/types.js.map +1 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +21 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/nest/index.d.ts +4 -0
  22. package/dist/nest/index.d.ts.map +1 -0
  23. package/dist/nest/index.js +20 -0
  24. package/dist/nest/index.js.map +1 -0
  25. package/dist/nest/otel-logger.service.d.ts +9 -0
  26. package/dist/nest/otel-logger.service.d.ts.map +1 -0
  27. package/dist/nest/otel-logger.service.js +32 -0
  28. package/dist/nest/otel-logger.service.js.map +1 -0
  29. package/dist/nest/otel-tracer.service.d.ts +10 -0
  30. package/dist/nest/otel-tracer.service.d.ts.map +1 -0
  31. package/dist/nest/otel-tracer.service.js +33 -0
  32. package/dist/nest/otel-tracer.service.js.map +1 -0
  33. package/dist/nest/otel.module.d.ts +3 -0
  34. package/dist/nest/otel.module.d.ts.map +1 -0
  35. package/dist/nest/otel.module.js +23 -0
  36. package/dist/nest/otel.module.js.map +1 -0
  37. package/package.json +32 -0
  38. package/src/core/index.ts +3 -0
  39. package/src/core/logger.ts +241 -0
  40. package/src/core/tracer.ts +213 -0
  41. package/src/core/types.ts +40 -0
  42. package/src/index.ts +5 -0
  43. package/src/nest/index.ts +3 -0
  44. package/src/nest/otel-logger.service.ts +16 -0
  45. package/src/nest/otel-tracer.service.ts +20 -0
  46. 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,3 @@
1
+ export declare class OtelModule {
2
+ }
3
+ //# sourceMappingURL=otel.module.d.ts.map
@@ -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,3 @@
1
+ export * from './types';
2
+ export * from './logger';
3
+ export * from './tracer';
@@ -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,5 @@
1
+ // Core (framework-agnostic)
2
+ export * from './core';
3
+
4
+ // NestJS integration
5
+ export * from './nest';
@@ -0,0 +1,3 @@
1
+ export * from './otel.module';
2
+ export * from './otel-logger.service';
3
+ export * from './otel-tracer.service';
@@ -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
+ }