@qlibs/apm 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @qlibs/apm
2
+
3
+ APM for NestJS apps: structured logging, distributed tracing, and error
4
+ monitoring, built on [OpenTelemetry](https://opentelemetry.io/) (OTLP) +
5
+ [Winston](https://github.com/winstonjs/winston).
6
+
7
+ It is vendor-neutral: point `OTEL_EXPORTER_OTLP_ENDPOINT` at **SigNoz**,
8
+ **Elastic APM Server** (has native OTLP ingestion), **Jaeger**, **Grafana
9
+ Tempo**, or any generic **OpenTelemetry Collector** — no code changes needed,
10
+ only env vars. If you don't set an endpoint, the app runs exactly as before
11
+ with zero overhead (tracing/remote-log transports are simply skipped).
12
+
13
+ What gets traced automatically (no business-logic changes required):
14
+ - Inbound HTTP requests (Express/Nest)
15
+ - Outbound HTTP calls (axios, fetch, node `http`/`https`)
16
+ - Postgres queries (instruments the `pg` driver, so Sequelize/TypeORM queries show up too)
17
+ - Redis (`ioredis` and `redis` clients)
18
+ - RabbitMQ (`amqplib` publish/consume)
19
+
20
+ Metrics (RED dashboards — latency, error rate, throughput) are intentionally
21
+ **not** exported separately: SigNoz and Elastic APM both derive these from
22
+ trace data automatically. If you need custom business metrics later, add an
23
+ `@opentelemetry/sdk-metrics` + OTLP metrics exporter next to `tracing.ts` —
24
+ the config/resource plumbing here already gives you everything needed to
25
+ wire that in.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ yarn add @qlibs/apm
31
+ ```
32
+
33
+ `@nestjs/common` and `rxjs` are peer dependencies (your Nest app already has
34
+ them). Everything else (OpenTelemetry packages, Winston) ships as a real
35
+ dependency of this package.
36
+
37
+ ## Usage
38
+
39
+ 1. **`main.ts`** — add as the literal first import, before anything else
40
+ (`module-alias/register`, `@nestjs/core`, etc. — the OTel SDK instruments
41
+ `http`/`pg`/`ioredis`/`amqplib` by patching them at require-time, so
42
+ anything imported earlier is invisible to tracing):
43
+ ```ts
44
+ import '@qlibs/apm/tracing';
45
+ import { NestFactory } from '@nestjs/core';
46
+ import { ApmLoggerService, ApmLoggingInterceptor, ApmExceptionFilter } from '@qlibs/apm';
47
+ // ...
48
+
49
+ async function bootstrap() {
50
+ const app = await NestFactory.create(AppModule, { bufferLogs: true });
51
+ app.useLogger(app.get(ApmLoggerService));
52
+ app.useGlobalInterceptors(new ApmLoggingInterceptor());
53
+ app.useGlobalFilters(new ApmExceptionFilter()); // skip if you have your own filter — see step 3
54
+ // ...
55
+ }
56
+ ```
57
+ 2. **`app.module.ts`** — add `ApmModule` to `imports` (makes `ApmLoggerService`
58
+ injectable into your own providers via constructor injection).
59
+ 3. **If your app already has a custom exception filter**, don't register
60
+ `ApmExceptionFilter`. Instead add one line to the existing filter's `catch()`:
61
+ ```ts
62
+ import { recordException } from '@qlibs/apm';
63
+ // inside catch(ex, host) { ... }
64
+ recordException(ex, 'YourFilterName');
65
+ ```
66
+ 4. **Env vars**: copy the block below into your project's env file and adjust
67
+ `APM_SERVICE_NAME`.
68
+
69
+ ## Env var reference
70
+
71
+ | Variable | Default | Purpose |
72
+ |---|---|---|
73
+ | `APM_ENABLED` | `true` | Master switch. `false` fully disables tracing + remote log transports (console logging still works). |
74
+ | `APM_SERVICE_NAME` | `ats-be` | Service name shown in traces/logs — set per app, e.g. `my-app-api`. |
75
+ | `APM_ENVIRONMENT` | `NODE_ENV` | `deployment.environment` resource attribute. |
76
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | _(empty)_ | Base OTLP endpoint. Empty = tracing disabled. |
77
+ | `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | `http/protobuf` (port 4318 typically) or `grpc` (port 4317). Must match what your collector listens on. |
78
+ | `OTEL_EXPORTER_OTLP_HEADERS` | _(empty)_ | Comma-separated `key=value` pairs, e.g. ingestion keys / auth tokens. |
79
+ | `LOG_LEVEL` | `info` (`debug` locally) | Winston log level. |
80
+ | `APM_LOG_FORMAT` | `pretty` locally, `json` otherwise | `pretty` = colorized console; `json` = structured stdout. |
81
+ | `APM_LOG_TRANSPORT` | `stdout` | Comma list of `stdout`, `elasticsearch`, `otlp`. Multiple can be active at once. |
82
+ | `ELASTICSEARCH_NODE` / `_USERNAME` / `_PASSWORD` / `_INDEX` | _(empty)_ | Only used when `APM_LOG_TRANSPORT` includes `elasticsearch`. |
83
+ | `APM_HTTP_IGNORE_PATHS` | `/health,/favicon.ico` | Comma list of path prefixes excluded from the HTTP access log. |
84
+
85
+ ## Pointing at a specific backend
86
+
87
+ **SigNoz** (self-hosted, OTLP-native collector):
88
+ ```
89
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
90
+ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
91
+ ```
92
+ Traces, and — if `APM_LOG_TRANSPORT=otlp` — logs, both flow through the same
93
+ collector endpoint. Nothing else to configure; SigNoz derives service
94
+ dashboards (latency/error-rate/throughput) from the trace data.
95
+
96
+ **Elastic APM Server / Elastic Cloud** (native OTLP ingestion since 8.x):
97
+ ```
98
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8200
99
+ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
100
+ OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <secret-token>
101
+ ```
102
+ For logs into plain Elasticsearch/Kibana (not via APM Server), use the
103
+ `elasticsearch` transport instead:
104
+ ```
105
+ APM_LOG_TRANSPORT=stdout,elasticsearch
106
+ ELASTICSEARCH_NODE=https://localhost:9200
107
+ ELASTICSEARCH_USERNAME=elastic
108
+ ELASTICSEARCH_PASSWORD=changeme
109
+ ```
110
+
111
+ **Generic OTel Collector / Jaeger / Tempo**: same as SigNoz — just point
112
+ `OTEL_EXPORTER_OTLP_ENDPOINT` at the collector's OTLP receiver.
113
+
114
+ **No collector running yet / local dev**: leave `OTEL_EXPORTER_OTLP_ENDPOINT`
115
+ empty. Logs still print (pretty, to console); tracing and error-span
116
+ recording are skipped with no crash and no retry noise.
117
+
118
+ ## Reference integration
119
+
120
+ See [qbit-tech/ats-be](https://github.com/qbit-tech/ats-be) — `apps/base`,
121
+ `apps/scheduler`, `apps/worker` all consume this package. `apps/base` is the
122
+ worked example for wiring into a project that already has its own exception
123
+ filter (`apps/base/src/core/output.filter.ts`); `apps/worker`/`apps/scheduler`
124
+ are the simpler "no existing filter" case.
125
+
126
+ ## Troubleshooting
127
+
128
+ - **No traces showing up**: confirm the app can reach `OTEL_EXPORTER_OTLP_ENDPOINT`
129
+ from wherever it's actually running (container networking — `localhost`
130
+ inside a container is not the host), and that `OTEL_EXPORTER_OTLP_PROTOCOL`
131
+ matches the port (4318 = http/protobuf, 4317 = grpc — sending http/protobuf
132
+ to a grpc-only port silently fails).
133
+ - **Logs missing `trace_id`**: `trace_id`/`span_id` are only injected when
134
+ there's an active span — that requires `@qlibs/apm/tracing` to have
135
+ actually started (i.e. `OTEL_EXPORTER_OTLP_ENDPOINT` set) and the log call
136
+ to happen inside a traced context (an HTTP request, a traced RabbitMQ
137
+ handler, etc.).
138
+ - **App fails to start after adding this**: check `@qlibs/apm/tracing`
139
+ really is the first import in `main.ts` — importing it after other modules
140
+ that require `pg`/`http`/etc. won't crash the app, but silently disables
141
+ instrumentation for those modules.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ yarn install
147
+ yarn build # compiles src/ -> dist/
148
+ ```
149
+
150
+ Publishing is handled by CI (`.github/workflows/release.yml`) on push to
151
+ `main`, or manually via `yarn release`.
@@ -0,0 +1,30 @@
1
+ export type LogFormat = 'pretty' | 'json';
2
+ export type LogTransportKind = 'stdout' | 'elasticsearch' | 'otlp';
3
+ export type OtlpProtocol = 'http/protobuf' | 'grpc';
4
+ export interface ApmConfig {
5
+ enabled: boolean;
6
+ serviceName: string;
7
+ serviceVersion: string;
8
+ environment: string;
9
+ tracing: {
10
+ enabled: boolean;
11
+ otlpEndpoint?: string;
12
+ protocol: OtlpProtocol;
13
+ headers: Record<string, string>;
14
+ };
15
+ logging: {
16
+ level: string;
17
+ format: LogFormat;
18
+ transports: LogTransportKind[];
19
+ elasticsearch: {
20
+ node?: string;
21
+ username?: string;
22
+ password?: string;
23
+ index: string;
24
+ };
25
+ };
26
+ http: {
27
+ ignorePaths: string[];
28
+ };
29
+ }
30
+ export declare function getApmConfig(): ApmConfig;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getApmConfig = getApmConfig;
4
+ function parseBoolean(value, fallback) {
5
+ if (value === undefined || value === '')
6
+ return fallback;
7
+ return value.toLowerCase() === 'true';
8
+ }
9
+ function parseList(value) {
10
+ if (!value)
11
+ return [];
12
+ return value
13
+ .split(',')
14
+ .map((v) => v.trim())
15
+ .filter(Boolean);
16
+ }
17
+ function parseHeaders(value) {
18
+ const headers = {};
19
+ for (const pair of parseList(value)) {
20
+ const idx = pair.indexOf('=');
21
+ if (idx === -1)
22
+ continue;
23
+ headers[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
24
+ }
25
+ return headers;
26
+ }
27
+ let cachedConfig;
28
+ function getApmConfig() {
29
+ if (cachedConfig)
30
+ return cachedConfig;
31
+ const env = process.env;
32
+ const nodeEnv = env.NODE_ENV || 'local';
33
+ const otlpEndpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT || undefined;
34
+ const transports = parseList(env.APM_LOG_TRANSPORT);
35
+ cachedConfig = {
36
+ enabled: parseBoolean(env.APM_ENABLED, true),
37
+ serviceName: env.APM_SERVICE_NAME || 'ats-be',
38
+ serviceVersion: env.APM_SERVICE_VERSION || '0.0.0',
39
+ environment: env.APM_ENVIRONMENT || nodeEnv,
40
+ tracing: {
41
+ enabled: parseBoolean(env.APM_TRACING_ENABLED, !!otlpEndpoint),
42
+ otlpEndpoint,
43
+ protocol: env.OTEL_EXPORTER_OTLP_PROTOCOL || 'http/protobuf',
44
+ headers: parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS),
45
+ },
46
+ logging: {
47
+ level: env.LOG_LEVEL || (nodeEnv === 'local' ? 'debug' : 'info'),
48
+ format: env.APM_LOG_FORMAT ||
49
+ (nodeEnv === 'local' || nodeEnv === 'development' ? 'pretty' : 'json'),
50
+ transports: transports.length ? transports : ['stdout'],
51
+ elasticsearch: {
52
+ node: env.ELASTICSEARCH_NODE || undefined,
53
+ username: env.ELASTICSEARCH_USERNAME || undefined,
54
+ password: env.ELASTICSEARCH_PASSWORD || undefined,
55
+ index: env.ELASTICSEARCH_INDEX || `${env.APM_SERVICE_NAME || 'ats-be'}-logs`,
56
+ },
57
+ },
58
+ http: {
59
+ ignorePaths: parseList(env.APM_HTTP_IGNORE_PATHS).length
60
+ ? parseList(env.APM_HTTP_IGNORE_PATHS)
61
+ : ['/health', '/favicon.ico'],
62
+ },
63
+ };
64
+ return cachedConfig;
65
+ }
@@ -0,0 +1,2 @@
1
+ export declare class ApmModule {
2
+ }
@@ -0,0 +1,21 @@
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.ApmModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const apm_logger_service_1 = require("./logger/apm-logger.service");
12
+ let ApmModule = class ApmModule {
13
+ };
14
+ exports.ApmModule = ApmModule;
15
+ exports.ApmModule = ApmModule = __decorate([
16
+ (0, common_1.Global)(),
17
+ (0, common_1.Module)({
18
+ providers: [apm_logger_service_1.ApmLoggerService],
19
+ exports: [apm_logger_service_1.ApmLoggerService],
20
+ })
21
+ ], ApmModule);
@@ -0,0 +1,4 @@
1
+ import { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
2
+ export declare class ApmExceptionFilter implements ExceptionFilter {
3
+ catch(err: Error, host: ArgumentsHost): void;
4
+ }
@@ -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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ApmExceptionFilter = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const record_exception_1 = require("../record-exception");
12
+ let ApmExceptionFilter = class ApmExceptionFilter {
13
+ catch(err, host) {
14
+ (0, record_exception_1.recordException)(err, 'ApmExceptionFilter');
15
+ const ctx = host.switchToHttp();
16
+ const response = ctx.getResponse();
17
+ if (!(response === null || response === void 0 ? void 0 : response.status))
18
+ return;
19
+ const status = err instanceof common_1.HttpException ? err.getStatus() : 500;
20
+ const body = err instanceof common_1.HttpException
21
+ ? err.getResponse()
22
+ : {
23
+ code: 'err_general',
24
+ message: 'Please contact our administrator',
25
+ errors: [],
26
+ };
27
+ response.status(status).json(body);
28
+ }
29
+ };
30
+ exports.ApmExceptionFilter = ApmExceptionFilter;
31
+ exports.ApmExceptionFilter = ApmExceptionFilter = __decorate([
32
+ (0, common_1.Catch)()
33
+ ], ApmExceptionFilter);
@@ -0,0 +1,4 @@
1
+ import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2
+ export declare class ApmLoggingInterceptor implements NestInterceptor {
3
+ intercept(context: ExecutionContext, next: CallHandler): import("rxjs").Observable<any>;
4
+ }
@@ -0,0 +1,60 @@
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.ApmLoggingInterceptor = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const rxjs_1 = require("rxjs");
12
+ const api_1 = require("@opentelemetry/api");
13
+ const apm_config_1 = require("../apm.config");
14
+ const winston_instance_1 = require("../logger/winston.instance");
15
+ let ApmLoggingInterceptor = class ApmLoggingInterceptor {
16
+ intercept(context, next) {
17
+ if (context.getType() !== 'http') {
18
+ return next.handle();
19
+ }
20
+ const request = context.switchToHttp().getRequest();
21
+ const { method, originalUrl, url, ip } = request;
22
+ const path = originalUrl || url;
23
+ const ignorePaths = (0, apm_config_1.getApmConfig)().http.ignorePaths;
24
+ if (ignorePaths.some((p) => path === null || path === void 0 ? void 0 : path.startsWith(p))) {
25
+ return next.handle();
26
+ }
27
+ const start = Date.now();
28
+ const span = api_1.trace.getActiveSpan();
29
+ const traceId = span === null || span === void 0 ? void 0 : span.spanContext().traceId;
30
+ return next.handle().pipe((0, rxjs_1.tap)({
31
+ next: () => {
32
+ const response = context.switchToHttp().getResponse();
33
+ winston_instance_1.winstonLogger.http(`${method} ${path} ${response.statusCode}`, {
34
+ context: 'HTTP',
35
+ method,
36
+ path,
37
+ statusCode: response.statusCode,
38
+ durationMs: Date.now() - start,
39
+ ip,
40
+ trace_id: traceId,
41
+ });
42
+ },
43
+ error: (err) => {
44
+ winston_instance_1.winstonLogger.http(`${method} ${path} failed`, {
45
+ context: 'HTTP',
46
+ method,
47
+ path,
48
+ durationMs: Date.now() - start,
49
+ ip,
50
+ trace_id: traceId,
51
+ error: err === null || err === void 0 ? void 0 : err.message,
52
+ });
53
+ },
54
+ }));
55
+ }
56
+ };
57
+ exports.ApmLoggingInterceptor = ApmLoggingInterceptor;
58
+ exports.ApmLoggingInterceptor = ApmLoggingInterceptor = __decorate([
59
+ (0, common_1.Injectable)()
60
+ ], ApmLoggingInterceptor);
@@ -0,0 +1,6 @@
1
+ export * from './apm.config';
2
+ export * from './apm.module';
3
+ export * from './record-exception';
4
+ export * from './logger/apm-logger.service';
5
+ export * from './http/apm-logging.interceptor';
6
+ export * from './filters/apm-exception.filter';
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./apm.config"), exports);
18
+ __exportStar(require("./apm.module"), exports);
19
+ __exportStar(require("./record-exception"), exports);
20
+ __exportStar(require("./logger/apm-logger.service"), exports);
21
+ __exportStar(require("./http/apm-logging.interceptor"), exports);
22
+ __exportStar(require("./filters/apm-exception.filter"), exports);
@@ -0,0 +1,9 @@
1
+ import { LoggerService } from '@nestjs/common';
2
+ export declare class ApmLoggerService implements LoggerService {
3
+ log(message: any, context?: string): void;
4
+ error(message: any, trace?: string, context?: string): void;
5
+ warn(message: any, context?: string): void;
6
+ debug(message: any, context?: string): void;
7
+ verbose(message: any, context?: string): void;
8
+ private stringify;
9
+ }
@@ -0,0 +1,44 @@
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.ApmLoggerService = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const winston_instance_1 = require("./winston.instance");
12
+ let ApmLoggerService = class ApmLoggerService {
13
+ log(message, context) {
14
+ winston_instance_1.winstonLogger.info(this.stringify(message), { context });
15
+ }
16
+ error(message, trace, context) {
17
+ winston_instance_1.winstonLogger.error(this.stringify(message), { trace, context });
18
+ }
19
+ warn(message, context) {
20
+ winston_instance_1.winstonLogger.warn(this.stringify(message), { context });
21
+ }
22
+ debug(message, context) {
23
+ winston_instance_1.winstonLogger.debug(this.stringify(message), { context });
24
+ }
25
+ verbose(message, context) {
26
+ winston_instance_1.winstonLogger.verbose(this.stringify(message), { context });
27
+ }
28
+ stringify(message) {
29
+ if (typeof message === 'string')
30
+ return message;
31
+ if (message instanceof Error)
32
+ return message.stack || message.message;
33
+ try {
34
+ return JSON.stringify(message);
35
+ }
36
+ catch (_a) {
37
+ return String(message);
38
+ }
39
+ }
40
+ };
41
+ exports.ApmLoggerService = ApmLoggerService;
42
+ exports.ApmLoggerService = ApmLoggerService = __decorate([
43
+ (0, common_1.Injectable)()
44
+ ], ApmLoggerService);
@@ -0,0 +1,5 @@
1
+ declare const OtlpLogTransport_base: any;
2
+ export declare class OtlpLogTransport extends OtlpLogTransport_base {
3
+ log(info: Record<string, any>, callback: () => void): void;
4
+ }
5
+ export {};
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __rest = (this && this.__rest) || function (s, e) {
36
+ var t = {};
37
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
38
+ t[p] = s[p];
39
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
40
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
41
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
42
+ t[p[i]] = s[p[i]];
43
+ }
44
+ return t;
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.OtlpLogTransport = void 0;
48
+ const Transport = __importStar(require("winston-transport"));
49
+ const api_1 = require("@opentelemetry/api");
50
+ const apm_config_1 = require("../apm.config");
51
+ const SEVERITY_MAP = {
52
+ error: { number: 17, text: 'ERROR' },
53
+ warn: { number: 13, text: 'WARN' },
54
+ info: { number: 9, text: 'INFO' },
55
+ http: { number: 9, text: 'INFO' },
56
+ verbose: { number: 5, text: 'DEBUG' },
57
+ debug: { number: 5, text: 'DEBUG' },
58
+ silly: { number: 1, text: 'TRACE' },
59
+ };
60
+ let otelLogger;
61
+ function getOtelLogger() {
62
+ if (otelLogger)
63
+ return otelLogger;
64
+ const config = (0, apm_config_1.getApmConfig)();
65
+ const { LoggerProvider, BatchLogRecordProcessor, } = require('@opentelemetry/sdk-logs');
66
+ const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
67
+ const { resourceFromAttributes } = require('@opentelemetry/resources');
68
+ const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
69
+ const base = (config.tracing.otlpEndpoint || '').replace(/\/$/, '');
70
+ const exporter = new OTLPLogExporter({
71
+ url: `${base}/v1/logs`,
72
+ headers: config.tracing.headers,
73
+ });
74
+ const provider = new LoggerProvider({
75
+ resource: resourceFromAttributes({
76
+ [ATTR_SERVICE_NAME]: config.serviceName,
77
+ }),
78
+ });
79
+ provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));
80
+ otelLogger = provider.getLogger(config.serviceName);
81
+ return otelLogger;
82
+ }
83
+ class OtlpLogTransport extends Transport {
84
+ log(info, callback) {
85
+ setImmediate(() => this.emit('logged', info));
86
+ const { level, message } = info, rest = __rest(info, ["level", "message"]);
87
+ const severity = SEVERITY_MAP[level] || SEVERITY_MAP.info;
88
+ try {
89
+ getOtelLogger().emit({
90
+ severityNumber: severity.number,
91
+ severityText: severity.text,
92
+ body: message,
93
+ attributes: rest,
94
+ context: api_1.context.active(),
95
+ });
96
+ }
97
+ catch (_a) {
98
+ }
99
+ callback();
100
+ }
101
+ }
102
+ exports.OtlpLogTransport = OtlpLogTransport;
@@ -0,0 +1,2 @@
1
+ import * as winston from 'winston';
2
+ export declare const traceContextFormat: winston.Logform.FormatWrap;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.traceContextFormat = void 0;
37
+ const winston = __importStar(require("winston"));
38
+ const api_1 = require("@opentelemetry/api");
39
+ exports.traceContextFormat = winston.format((info) => {
40
+ const span = api_1.trace.getActiveSpan();
41
+ if (span) {
42
+ const ctx = span.spanContext();
43
+ info.trace_id = ctx.traceId;
44
+ info.span_id = ctx.spanId;
45
+ }
46
+ return info;
47
+ });
@@ -0,0 +1,2 @@
1
+ import * as winston from 'winston';
2
+ export declare const winstonLogger: winston.Logger;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __rest = (this && this.__rest) || function (s, e) {
36
+ var t = {};
37
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
38
+ t[p] = s[p];
39
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
40
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
41
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
42
+ t[p[i]] = s[p[i]];
43
+ }
44
+ return t;
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.winstonLogger = void 0;
48
+ const winston = __importStar(require("winston"));
49
+ const apm_config_1 = require("../apm.config");
50
+ const trace_context_format_1 = require("./trace-context.format");
51
+ function buildConsoleTransport() {
52
+ const config = (0, apm_config_1.getApmConfig)();
53
+ const format = config.logging.format === 'pretty'
54
+ ? winston.format.combine((0, trace_context_format_1.traceContextFormat)(), winston.format.colorize(), winston.format.timestamp({ format: 'HH:mm:ss' }), winston.format.printf((_a) => {
55
+ var { timestamp, level, message, context, trace_id } = _a, meta = __rest(_a, ["timestamp", "level", "message", "context", "trace_id"]);
56
+ const ctx = context ? ` [${context}]` : '';
57
+ const trace = trace_id ? ` (trace=${trace_id})` : '';
58
+ const extra = Object.keys(meta).length
59
+ ? ` ${JSON.stringify(meta)}`
60
+ : '';
61
+ return `${timestamp} ${level}${ctx} ${message}${trace}${extra}`;
62
+ }))
63
+ : winston.format.combine((0, trace_context_format_1.traceContextFormat)(), winston.format.timestamp(), winston.format.json());
64
+ return new winston.transports.Console({ format });
65
+ }
66
+ function buildElasticsearchTransport() {
67
+ const config = (0, apm_config_1.getApmConfig)();
68
+ if (!config.logging.elasticsearch.node) {
69
+ console.warn('[apm] APM_LOG_TRANSPORT includes "elasticsearch" but ELASTICSEARCH_NODE is not set — skipping.');
70
+ return undefined;
71
+ }
72
+ const { ElasticsearchTransport } = require('winston-elasticsearch');
73
+ return new ElasticsearchTransport({
74
+ level: config.logging.level,
75
+ indexPrefix: config.logging.elasticsearch.index,
76
+ clientOpts: {
77
+ node: config.logging.elasticsearch.node,
78
+ auth: config.logging.elasticsearch.username &&
79
+ config.logging.elasticsearch.password
80
+ ? {
81
+ username: config.logging.elasticsearch.username,
82
+ password: config.logging.elasticsearch.password,
83
+ }
84
+ : undefined,
85
+ },
86
+ format: winston.format.combine((0, trace_context_format_1.traceContextFormat)(), winston.format.json()),
87
+ });
88
+ }
89
+ function buildOtlpTransport() {
90
+ const config = (0, apm_config_1.getApmConfig)();
91
+ if (!config.tracing.otlpEndpoint) {
92
+ console.warn('[apm] APM_LOG_TRANSPORT includes "otlp" but OTEL_EXPORTER_OTLP_ENDPOINT is not set — skipping.');
93
+ return undefined;
94
+ }
95
+ const { OtlpLogTransport } = require('./otlp-log.transport');
96
+ return new OtlpLogTransport({ level: config.logging.level });
97
+ }
98
+ function buildTransports() {
99
+ const config = (0, apm_config_1.getApmConfig)();
100
+ const transports = [];
101
+ for (const kind of config.logging.transports) {
102
+ if (kind === 'stdout')
103
+ transports.push(buildConsoleTransport());
104
+ else if (kind === 'elasticsearch') {
105
+ const t = buildElasticsearchTransport();
106
+ if (t)
107
+ transports.push(t);
108
+ }
109
+ else if (kind === 'otlp') {
110
+ const t = buildOtlpTransport();
111
+ if (t)
112
+ transports.push(t);
113
+ }
114
+ }
115
+ if (transports.length === 0)
116
+ transports.push(buildConsoleTransport());
117
+ return transports;
118
+ }
119
+ const config = (0, apm_config_1.getApmConfig)();
120
+ exports.winstonLogger = winston.createLogger({
121
+ level: config.logging.level,
122
+ defaultMeta: { service: config.serviceName, environment: config.environment },
123
+ transports: buildTransports(),
124
+ });
@@ -0,0 +1 @@
1
+ export declare function recordException(err: unknown, context?: string): void;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordException = recordException;
4
+ const api_1 = require("@opentelemetry/api");
5
+ const winston_instance_1 = require("./logger/winston.instance");
6
+ function recordException(err, context) {
7
+ const error = err instanceof Error ? err : new Error(String(err));
8
+ const span = api_1.trace.getActiveSpan();
9
+ if (span) {
10
+ span.recordException(error);
11
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
12
+ }
13
+ winston_instance_1.winstonLogger.error(error.message, {
14
+ context: context || 'ExceptionFilter',
15
+ stack: error.stack,
16
+ name: error.name,
17
+ });
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const apm_config_1 = require("./apm.config");
4
+ const config = (0, apm_config_1.getApmConfig)();
5
+ if (config.enabled && config.tracing.enabled && config.tracing.otlpEndpoint) {
6
+ const { NodeSDK } = require('@opentelemetry/sdk-node');
7
+ const { getNodeAutoInstrumentations, } = require('@opentelemetry/auto-instrumentations-node');
8
+ const { OTLPTraceExporter, } = require('@opentelemetry/exporter-trace-otlp-http');
9
+ const { resourceFromAttributes } = require('@opentelemetry/resources');
10
+ const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } = require('@opentelemetry/semantic-conventions');
11
+ const traceExporter = new OTLPTraceExporter({
12
+ url: config.tracing.protocol === 'grpc'
13
+ ? config.tracing.otlpEndpoint
14
+ : `${config.tracing.otlpEndpoint.replace(/\/$/, '')}/v1/traces`,
15
+ headers: config.tracing.headers,
16
+ });
17
+ const sdk = new NodeSDK({
18
+ resource: resourceFromAttributes({
19
+ [ATTR_SERVICE_NAME]: config.serviceName,
20
+ [ATTR_SERVICE_VERSION]: config.serviceVersion,
21
+ 'deployment.environment': config.environment,
22
+ }),
23
+ traceExporter,
24
+ instrumentations: [
25
+ getNodeAutoInstrumentations({
26
+ '@opentelemetry/instrumentation-fs': { enabled: false },
27
+ }),
28
+ ],
29
+ });
30
+ sdk.start();
31
+ const shutdown = () => {
32
+ sdk
33
+ .shutdown()
34
+ .catch((err) => console.error('Error shutting down OTel SDK', err))
35
+ .finally(() => process.exit(0));
36
+ };
37
+ process.on('SIGTERM', shutdown);
38
+ process.on('SIGINT', shutdown);
39
+ console.info(`[apm] tracing enabled -> ${config.tracing.otlpEndpoint} (service: ${config.serviceName})`);
40
+ }
41
+ else {
42
+ console.info('[apm] tracing disabled (set OTEL_EXPORTER_OTLP_ENDPOINT to enable)');
43
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@qlibs/apm",
3
+ "version": "0.1.1",
4
+ "description": "Vendor-neutral APM for NestJS: OpenTelemetry tracing (OTLP) + structured logging + error monitoring. Works with SigNoz, Elastic/Kibana, Jaeger, or any OTel collector.",
5
+ "author": "QBIT Developer",
6
+ "license": "MIT",
7
+ "url": "https://github.com/qbit-tech/qlibs-apm",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "/dist",
12
+ "tracing.js",
13
+ "tracing.d.ts"
14
+ ],
15
+ "scripts": {
16
+ "build": "rimraf -rf dist && tsc -p tsconfig.json",
17
+ "format": "prettier --write \"**/*.ts\"",
18
+ "lint": "eslint \"{src,test}/**/*.ts\" --fix",
19
+ "prepublish:npm": "npm run build",
20
+ "publish:npm": "npm publish --access public",
21
+ "prepublish:next": "npm run build",
22
+ "publish:next": "npm publish --access public --tag next",
23
+ "prerelease": "npm run build",
24
+ "release": "release-it",
25
+ "prepare": "husky install && npm run build"
26
+ },
27
+ "dependencies": {
28
+ "@opentelemetry/api": "^1.9.1",
29
+ "@opentelemetry/api-logs": "^0.221.0",
30
+ "@opentelemetry/auto-instrumentations-node": "^0.79.0",
31
+ "@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
32
+ "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
33
+ "@opentelemetry/resources": "^2.10.0",
34
+ "@opentelemetry/sdk-logs": "^0.221.0",
35
+ "@opentelemetry/sdk-node": "^0.221.0",
36
+ "@opentelemetry/semantic-conventions": "^1.43.0",
37
+ "winston": "^3.19.0",
38
+ "winston-elasticsearch": "^0.19.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
42
+ "rxjs": "^7.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@nestjs/common": "^10.0.0",
46
+ "@types/express": "^4.17.17",
47
+ "@types/node": "^20.3.1",
48
+ "express": "^4.19.2",
49
+ "husky": "^8.0.3",
50
+ "reflect-metadata": "^0.1.13",
51
+ "release-it": "16.1.5",
52
+ "rimraf": "5.0.1",
53
+ "rxjs": "^7.8.1",
54
+ "typescript": "^5.1.3"
55
+ },
56
+ "publishConfig": {
57
+ "registry": "https://registry.npmjs.org"
58
+ }
59
+ }
package/tracing.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Side-effect-only module — see tracing.js / dist/tracing.d.ts.
2
+ export {};
package/tracing.js ADDED
@@ -0,0 +1,4 @@
1
+ // Shim so consumers can `import '@qlibs/apm/tracing'` directly (needed since
2
+ // this must be the very first import in main.ts, before the rest of the
3
+ // package's barrel). See README.md.
4
+ require('./dist/tracing');