@qlibs/apm 0.1.2 → 1.1.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/README.md CHANGED
@@ -19,7 +19,8 @@ What gets traced automatically (no business-logic changes required):
19
19
 
20
20
  Metrics (RED dashboards — latency, error rate, throughput) are intentionally
21
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
22
+ trace data automatically. The OTel SDK therefore ships traces only
23
+ (`OTEL_METRICS_EXPORTER` and `OTEL_LOGS_EXPORTER` default to `none`). If you need custom business metrics later, add an
23
24
  `@opentelemetry/sdk-metrics` + OTLP metrics exporter next to `tracing.ts` —
24
25
  the config/resource plumbing here already gives you everything needed to
25
26
  wire that in.
@@ -70,17 +71,209 @@ dependency of this package.
70
71
 
71
72
  | Variable | Default | Purpose |
72
73
  |---|---|---|
73
- | `APM_ENABLED` | `true` | Master switch. `false` fully disables tracing + remote log transports (console logging still works). |
74
+ | `APM_ENABLED` | `true` | `false` disables tracing. Log transports are controlled only by `APM_LOG_TRANSPORT`. |
74
75
  | `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
+ | `APM_ENVIRONMENT` | `NODE_ENV`, else `production` | `deployment.environment` resource attribute. Also drives the log level/format defaults below. Unset `NODE_ENV` is treated as `production` (never as local). |
76
77
  | `OTEL_EXPORTER_OTLP_ENDPOINT` | _(empty)_ | Base OTLP endpoint. Empty = tracing disabled. |
77
78
  | `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | `http/protobuf` (port 4318 typically) or `grpc` (port 4317). Must match what your collector listens on. |
78
79
  | `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. |
80
+ | `LOG_LEVEL` | `info` (`debug` when env is `local`) | Winston log level (`error`, `warn`, `info`, `http`, `verbose`, `debug`). At `info`, Nest `verbose`/`debug` logs are dropped — set `verbose` or `debug` to see them. HTTP access logs and Nest `fatal` (logged as `error` with `fatal: true`) are always visible at `info`. On the console, `error`/`warn` go to stderr and everything else to stdout. |
81
+ | `APM_LOG_FORMAT` | `pretty` for `local`/`development`, `json` otherwise | `pretty` = colorized console; `json` = structured stdout. |
82
+ | `APM_LOG_TRANSPORT` | `stdout` | Comma list of `stdout`, `elasticsearch`, `otlp`. Multiple can be active at once. `none` = the APM writes no log lines of its own (no access log, no extra error line); `ApmLoggerService` then just forwards to Nest's default console logger, so console output appears once, in Nest's format. Traces are unaffected. |
83
+ | `ELASTICSEARCH_NODE` / `_USERNAME` / `_PASSWORD` / `_INDEX` | _(empty)_ | Only used when `APM_LOG_TRANSPORT` includes `elasticsearch`. While ES is unreachable at most 1000 log lines are buffered (the rest are dropped) and each is retried 5 times. Shipping via stdout + Filebeat/Fluent Bit is still preferred in production. |
84
+ | `APM_HTTP_IGNORE_PATHS` | `/health,/favicon.ico` | Comma list of path prefixes excluded from the HTTP access log and from tracing. |
85
+ | `OTEL_NODE_ENABLED_INSTRUMENTATIONS` | `http,undici,express,nestjs-core,pg,ioredis,redis,amqplib` | Which auto-instrumentations to load (names without the `@opentelemetry/instrumentation-` prefix). Add e.g. `mongodb`, `kafkajs` if your app needs them. |
86
+ | `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG` | `parentbased_always_on` | Standard OTel sampling. For high-traffic services use `parentbased_traceidratio` with e.g. `0.1` (10%). |
87
+
88
+ ## Recommended setup
89
+
90
+ ### Production
91
+
92
+ ```env
93
+ NODE_ENV=production
94
+ APM_SERVICE_NAME=my-app-api
95
+ APM_SERVICE_VERSION=1.4.2
96
+ LOG_LEVEL=warn
97
+ APM_LOG_TRANSPORT=none
98
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
99
+ # Optional, for high-traffic services (> ~50 req/s):
100
+ # OTEL_TRACES_SAMPLER=parentbased_traceidratio
101
+ # OTEL_TRACES_SAMPLER_ARG=0.2
102
+ ```
103
+
104
+ | Destination | What you get |
105
+ |---|---|
106
+ | **pm2 / docker logs** | Only `ERROR`, `WARN` and `FATAL`, in Nest's own format (`[Nest] 123 - ... ERROR [OrderService] ...`), each line **once**. Errors/fatal go to stderr (`pm2 logs --err`), warnings to stdout. No per-request access log, no `log`/`verbose`/`debug`. Plus one startup line: `[apm] tracing enabled -> ...`. |
107
+ | **SigNoz — Traces** | Every request (except `/health`, `/favicon.ico`) including its Postgres, Redis, RabbitMQ and outbound HTTP calls. Failed requests are marked as errors with message and stack trace. Latency / error-rate / throughput dashboards are derived from these traces. |
108
+ | **SigNoz — Logs** | Nothing is sent. |
109
+ | **SigNoz — Metrics** | Nothing is sent. |
110
+ | **Deploy / restart** | Pending traces are flushed (max 5s), then Nest's graceful shutdown runs normally. |
111
+
112
+ In short: the terminal is for warnings and errors only; per-request detail
113
+ lives in SigNoz.
114
+
115
+ Requirements in the app:
116
+ - `import '@qlibs/apm/tracing'` as the first line of `main.ts`.
117
+ - `recordException()` or `ApmExceptionFilter` in place, so errors are marked
118
+ on the trace.
119
+ - If `main.ts` does **not** call `app.useLogger(app.get(ApmLoggerService))`,
120
+ `LOG_LEVEL` does not filter Nest's output — use
121
+ `NestFactory.create(AppModule, { logger: ['fatal', 'error', 'warn'] })`
122
+ instead.
123
+
124
+ On the server, enable log rotation as a safety net (`pm2 install
125
+ pm2-logrotate`, or `max-size` / `max-file` for Docker's `json-file` driver).
126
+
127
+ ### Development (shared dev / staging server)
128
+
129
+ ```env
130
+ NODE_ENV=development
131
+ APM_SERVICE_NAME=my-app-api
132
+ LOG_LEVEL=debug
133
+ APM_LOG_TRANSPORT=stdout
134
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
135
+ ```
136
+
137
+ | Destination | What you get |
138
+ |---|---|
139
+ | **pm2 / docker logs** | **All levels** (error … debug), colorized and human-readable, written by the APM — e.g. `10:21:05 info [HTTP] GET /api/orders 200 (trace=4bf92f...)`. Includes a **per-request access log** with duration and `trace_id`, so any log line can be looked up as a trace in SigNoz. Nest's own logs go through the same logger, so nothing is printed twice. |
140
+ | **SigNoz — Traces** | Every request, kept apart from production by `deployment.environment=development` (filter by environment in SigNoz). |
141
+ | **SigNoz — Logs / Metrics** | Nothing is sent. |
142
+
143
+ **Developer laptop:** use `NODE_ENV=local` and drop `LOG_LEVEL` (defaults to
144
+ `debug`) and `OTEL_EXPORTER_OTLP_ENDPOINT`. Logs stay colorized in the
145
+ terminal, tracing is off, nothing is sent anywhere.
146
+
147
+ ### At a glance
148
+
149
+ | | Production | Development |
150
+ |---|---|---|
151
+ | Who prints to the terminal | Nest (APM is silent) | APM |
152
+ | Levels shown | warn, error, fatal | all |
153
+ | Per-request access log | ❌ (use SigNoz) | ✅ |
154
+ | `trace_id` on log lines | ❌ | ✅ |
155
+ | Traces to SigNoz | ✅ | ✅ |
156
+ | Logs / metrics to SigNoz | ❌ | ❌ |
157
+
158
+ ## Example configurations
159
+
160
+ `LOG_LEVEL` is a threshold: it keeps that level and everything more severe
161
+ (`error` < `warn` < `info` < `http` < `verbose` < `debug` < `silly`). Nest's
162
+ `logger.log()` is `info`, `logger.fatal()` is `error`. Use lowercase values
163
+ exactly as listed — an unknown value (e.g. `WARN`, `log`, `trace`) currently
164
+ drops **all** logs, errors included.
165
+
166
+ What each setting affects:
167
+
168
+ | | stdout (pm2 / docker logs) | Logs in SigNoz | Traces in SigNoz |
169
+ |---|---|---|---|
170
+ | `LOG_LEVEL` | ✅ | ✅ | ❌ |
171
+ | `APM_LOG_TRANSPORT` | ✅ | ✅ (needs `otlp`) | ❌ |
172
+ | `OTEL_TRACES_SAMPLER` | ❌ | ❌ | ✅ |
173
+
174
+ Errors are still marked on the trace span by `recordException()` regardless of
175
+ `LOG_LEVEL`. After changing env vars, restart the app (pm2:
176
+ `pm2 restart <app> --update-env`).
177
+
178
+ **1. Traces only — Nest's own console logs, no duplicate APM log lines**
179
+ ```env
180
+ NODE_ENV=production
181
+ APM_SERVICE_NAME=my-app-api
182
+ LOG_LEVEL=warn
183
+ APM_LOG_TRANSPORT=none
184
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
185
+ ```
186
+ pm2/docker show only Nest's normal `[Nest] ... ERROR [Context] ...` lines, each
187
+ once. Errors still appear on the trace in SigNoz (status ERROR + exception
188
+ event) via `recordException()`. `LOG_LEVEL` keeps working if `main.ts` calls
189
+ `app.useLogger(app.get(ApmLoggerService))`; if it doesn't, filter Nest's output
190
+ with `NestFactory.create(AppModule, { logger: ['fatal', 'error', 'warn'] })`
191
+ instead. Trade-off: console lines carry no `trace_id`.
192
+
193
+ **2. Production, minimal — only warn + error in pm2/docker, traces to SigNoz**
194
+ ```env
195
+ NODE_ENV=production
196
+ APM_SERVICE_NAME=my-app-api
197
+ LOG_LEVEL=warn
198
+ APM_LOG_TRANSPORT=stdout
199
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
200
+ ```
201
+ Note: at `warn` the HTTP access log (`GET /api/... 200`, level `info`) is
202
+ dropped too.
203
+
204
+ **3. Production, standard — info + access log in pm2/docker, logs and traces to SigNoz**
205
+ ```env
206
+ NODE_ENV=production
207
+ APM_SERVICE_NAME=my-app-api
208
+ LOG_LEVEL=info
209
+ APM_LOG_TRANSPORT=stdout,otlp
210
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
211
+ ```
212
+
213
+ **4. Production, high traffic — trace only 10% of requests**
214
+ ```env
215
+ NODE_ENV=production
216
+ APM_SERVICE_NAME=my-app-api
217
+ LOG_LEVEL=warn
218
+ APM_LOG_TRANSPORT=stdout,otlp
219
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
220
+ OTEL_TRACES_SAMPLER=parentbased_traceidratio
221
+ OTEL_TRACES_SAMPLER_ARG=0.1
222
+ ```
223
+
224
+ **5. Staging, investigating a bug — everything**
225
+ ```env
226
+ NODE_ENV=staging
227
+ APM_SERVICE_NAME=my-app-api
228
+ LOG_LEVEL=debug
229
+ APM_LOG_TRANSPORT=stdout,otlp
230
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
231
+ ```
232
+
233
+ **6. Local development, no SigNoz**
234
+ ```env
235
+ NODE_ENV=local
236
+ APM_SERVICE_NAME=my-app-api
237
+ # LOG_LEVEL defaults to debug, format to colorized pretty output
238
+ # OTEL_EXPORTER_OTLP_ENDPOINT left empty -> tracing off
239
+ ```
240
+
241
+ **7. Logs to pm2/docker only, no tracing at all**
242
+ ```env
243
+ NODE_ENV=production
244
+ APM_SERVICE_NAME=my-app-api
245
+ LOG_LEVEL=warn
246
+ APM_LOG_TRANSPORT=stdout
247
+ # OTEL_EXPORTER_OTLP_ENDPOINT not set -> nothing is sent to SigNoz
248
+ ```
249
+
250
+ **8. Collector listening on gRPC (port 4317)**
251
+ ```env
252
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4317
253
+ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
254
+ ```
255
+
256
+ **9. Logs to Elasticsearch / Kibana**
257
+ ```env
258
+ NODE_ENV=production
259
+ APM_SERVICE_NAME=my-app-api
260
+ LOG_LEVEL=warn
261
+ APM_LOG_TRANSPORT=stdout,elasticsearch
262
+ ELASTICSEARCH_NODE=https://es.internal:9200
263
+ ELASTICSEARCH_USERNAME=elastic
264
+ ELASTICSEARCH_PASSWORD=changeme
265
+ ```
266
+
267
+ To stop shipping logs remotely, remove `otlp` / `elasticsearch` from
268
+ `APM_LOG_TRANSPORT` — `APM_ENABLED=false` currently only turns tracing off.
269
+
270
+ ## Shutdown
271
+
272
+ On `SIGTERM`/`SIGINT` the tracing SDK flushes pending spans (max 5s) but does
273
+ **not** call `process.exit()` — your app's own graceful shutdown (e.g.
274
+ `app.enableShutdownHooks()` in Nest) stays in control. If no other signal
275
+ handler is registered, the signal is re-raised after the flush so the process
276
+ still terminates normally.
84
277
 
85
278
  ## Pointing at a specific backend
86
279
 
@@ -1,5 +1,5 @@
1
1
  export type LogFormat = 'pretty' | 'json';
2
- export type LogTransportKind = 'stdout' | 'elasticsearch' | 'otlp';
2
+ export type LogTransportKind = 'stdout' | 'elasticsearch' | 'otlp' | 'none';
3
3
  export type OtlpProtocol = 'http/protobuf' | 'grpc';
4
4
  export interface ApmConfig {
5
5
  enabled: boolean;
@@ -13,6 +13,7 @@ export interface ApmConfig {
13
13
  headers: Record<string, string>;
14
14
  };
15
15
  logging: {
16
+ enabled: boolean;
16
17
  level: string;
17
18
  format: LogFormat;
18
19
  transports: LogTransportKind[];
@@ -29,7 +29,8 @@ function getApmConfig() {
29
29
  if (cachedConfig)
30
30
  return cachedConfig;
31
31
  const env = process.env;
32
- const nodeEnv = env.NODE_ENV || 'local';
32
+ const nodeEnv = env.APM_ENVIRONMENT || env.NODE_ENV || 'production';
33
+ const isLocal = nodeEnv === 'local' || nodeEnv === 'development';
33
34
  const otlpEndpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT || undefined;
34
35
  const transports = parseList(env.APM_LOG_TRANSPORT);
35
36
  cachedConfig = {
@@ -44,9 +45,9 @@ function getApmConfig() {
44
45
  headers: parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS),
45
46
  },
46
47
  logging: {
48
+ enabled: !transports.includes('none'),
47
49
  level: env.LOG_LEVEL || (nodeEnv === 'local' ? 'debug' : 'info'),
48
- format: env.APM_LOG_FORMAT ||
49
- (nodeEnv === 'local' || nodeEnv === 'development' ? 'pretty' : 'json'),
50
+ format: env.APM_LOG_FORMAT || (isLocal ? 'pretty' : 'json'),
50
51
  transports: transports.length ? transports : ['stdout'],
51
52
  elasticsearch: {
52
53
  node: env.ELASTICSEARCH_NODE || undefined,
@@ -14,7 +14,7 @@ let ApmExceptionFilter = class ApmExceptionFilter {
14
14
  (0, record_exception_1.recordException)(err, 'ApmExceptionFilter');
15
15
  const ctx = host.switchToHttp();
16
16
  const response = ctx.getResponse();
17
- if (!(response === null || response === void 0 ? void 0 : response.status))
17
+ if (!(response === null || response === void 0 ? void 0 : response.status) || response.headersSent)
18
18
  return;
19
19
  const status = err instanceof common_1.HttpException ? err.getStatus() : 500;
20
20
  const body = err instanceof common_1.HttpException
@@ -14,7 +14,7 @@ const apm_config_1 = require("../apm.config");
14
14
  const winston_instance_1 = require("../logger/winston.instance");
15
15
  let ApmLoggingInterceptor = class ApmLoggingInterceptor {
16
16
  intercept(context, next) {
17
- if (context.getType() !== 'http') {
17
+ if (context.getType() !== 'http' || !(0, apm_config_1.getApmConfig)().logging.enabled) {
18
18
  return next.handle();
19
19
  }
20
20
  const request = context.switchToHttp().getRequest();
@@ -30,7 +30,7 @@ let ApmLoggingInterceptor = class ApmLoggingInterceptor {
30
30
  return next.handle().pipe((0, rxjs_1.tap)({
31
31
  next: () => {
32
32
  const response = context.switchToHttp().getResponse();
33
- winston_instance_1.winstonLogger.http(`${method} ${path} ${response.statusCode}`, {
33
+ winston_instance_1.winstonLogger.info(`${method} ${path} ${response.statusCode}`, {
34
34
  context: 'HTTP',
35
35
  method,
36
36
  path,
@@ -41,7 +41,7 @@ let ApmLoggingInterceptor = class ApmLoggingInterceptor {
41
41
  });
42
42
  },
43
43
  error: (err) => {
44
- winston_instance_1.winstonLogger.http(`${method} ${path} failed`, {
44
+ winston_instance_1.winstonLogger.info(`${method} ${path} failed`, {
45
45
  context: 'HTTP',
46
46
  method,
47
47
  path,
@@ -1,9 +1,11 @@
1
1
  import { LoggerService } from '@nestjs/common';
2
2
  export declare class ApmLoggerService implements LoggerService {
3
+ private readonly fallback;
3
4
  log(message: any, context?: string): void;
4
5
  error(message: any, trace?: string, context?: string): void;
5
6
  warn(message: any, context?: string): void;
6
7
  debug(message: any, context?: string): void;
8
+ fatal(message: any, context?: string): void;
7
9
  verbose(message: any, context?: string): void;
8
10
  private stringify;
9
11
  }
@@ -8,21 +8,57 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.ApmLoggerService = void 0;
10
10
  const common_1 = require("@nestjs/common");
11
+ const apm_config_1 = require("../apm.config");
11
12
  const winston_instance_1 = require("./winston.instance");
13
+ const NEST_LEVELS = {
14
+ error: ['fatal', 'error'],
15
+ warn: ['fatal', 'error', 'warn'],
16
+ info: ['fatal', 'error', 'warn', 'log'],
17
+ http: ['fatal', 'error', 'warn', 'log'],
18
+ verbose: ['fatal', 'error', 'warn', 'log', 'verbose'],
19
+ debug: ['fatal', 'error', 'warn', 'log', 'verbose', 'debug'],
20
+ silly: ['fatal', 'error', 'warn', 'log', 'verbose', 'debug'],
21
+ };
22
+ function buildFallbackLogger() {
23
+ const { logging } = (0, apm_config_1.getApmConfig)();
24
+ if (logging.enabled)
25
+ return undefined;
26
+ return new common_1.ConsoleLogger(undefined, {
27
+ logLevels: NEST_LEVELS[logging.level] || NEST_LEVELS.info,
28
+ });
29
+ }
12
30
  let ApmLoggerService = class ApmLoggerService {
31
+ constructor() {
32
+ this.fallback = buildFallbackLogger();
33
+ }
13
34
  log(message, context) {
35
+ if (this.fallback)
36
+ return this.fallback.log(message, context);
14
37
  winston_instance_1.winstonLogger.info(this.stringify(message), { context });
15
38
  }
16
39
  error(message, trace, context) {
40
+ if (this.fallback)
41
+ return this.fallback.error(message, trace, context);
17
42
  winston_instance_1.winstonLogger.error(this.stringify(message), { trace, context });
18
43
  }
19
44
  warn(message, context) {
45
+ if (this.fallback)
46
+ return this.fallback.warn(message, context);
20
47
  winston_instance_1.winstonLogger.warn(this.stringify(message), { context });
21
48
  }
22
49
  debug(message, context) {
50
+ if (this.fallback)
51
+ return this.fallback.debug(message, context);
23
52
  winston_instance_1.winstonLogger.debug(this.stringify(message), { context });
24
53
  }
54
+ fatal(message, context) {
55
+ if (this.fallback)
56
+ return this.fallback.fatal(message, context);
57
+ winston_instance_1.winstonLogger.error(this.stringify(message), { context, fatal: true });
58
+ }
25
59
  verbose(message, context) {
60
+ if (this.fallback)
61
+ return this.fallback.verbose(message, context);
26
62
  winston_instance_1.winstonLogger.verbose(this.stringify(message), { context });
27
63
  }
28
64
  stringify(message) {
@@ -28,25 +28,33 @@ const SEVERITY_MAP = {
28
28
  silly: { number: 1, text: 'TRACE' },
29
29
  };
30
30
  let otelLogger;
31
+ let emitFailureReported = false;
31
32
  function getOtelLogger() {
32
33
  if (otelLogger)
33
34
  return otelLogger;
34
35
  const config = (0, apm_config_1.getApmConfig)();
35
36
  const { LoggerProvider, BatchLogRecordProcessor, } = require('@opentelemetry/sdk-logs');
36
- const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
37
37
  const { resourceFromAttributes } = require('@opentelemetry/resources');
38
38
  const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
39
- const base = (config.tracing.otlpEndpoint || '').replace(/\/$/, '');
40
- const exporter = new OTLPLogExporter({
41
- url: `${base}/v1/logs`,
42
- headers: config.tracing.headers,
43
- });
39
+ let exporter;
40
+ if (config.tracing.protocol === 'grpc') {
41
+ const { OTLPLogExporter, } = require('@opentelemetry/exporter-logs-otlp-grpc');
42
+ exporter = new OTLPLogExporter({ url: config.tracing.otlpEndpoint });
43
+ }
44
+ else {
45
+ const { OTLPLogExporter, } = require('@opentelemetry/exporter-logs-otlp-http');
46
+ const base = (config.tracing.otlpEndpoint || '').replace(/\/$/, '');
47
+ exporter = new OTLPLogExporter({
48
+ url: `${base}/v1/logs`,
49
+ headers: config.tracing.headers,
50
+ });
51
+ }
44
52
  const provider = new LoggerProvider({
45
53
  resource: resourceFromAttributes({
46
54
  [ATTR_SERVICE_NAME]: config.serviceName,
47
55
  }),
56
+ processors: [new BatchLogRecordProcessor({ exporter })],
48
57
  });
49
- provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));
50
58
  otelLogger = provider.getLogger(config.serviceName);
51
59
  return otelLogger;
52
60
  }
@@ -64,7 +72,11 @@ class OtlpLogTransport extends winston_transport_1.default {
64
72
  context: api_1.context.active(),
65
73
  });
66
74
  }
67
- catch (_a) {
75
+ catch (err) {
76
+ if (!emitFailureReported) {
77
+ emitFailureReported = true;
78
+ console.error('[apm] otlp log transport failed, logs are not being exported:', err instanceof Error ? err.message : err);
79
+ }
68
80
  }
69
81
  callback();
70
82
  }
@@ -61,7 +61,10 @@ function buildConsoleTransport() {
61
61
  return `${timestamp} ${level}${ctx} ${message}${trace}${extra}`;
62
62
  }))
63
63
  : winston.format.combine((0, trace_context_format_1.traceContextFormat)(), winston.format.timestamp(), winston.format.json());
64
- return new winston.transports.Console({ format });
64
+ return new winston.transports.Console({
65
+ format,
66
+ stderrLevels: ['error', 'warn'],
67
+ });
65
68
  }
66
69
  function buildElasticsearchTransport() {
67
70
  const config = (0, apm_config_1.getApmConfig)();
@@ -73,6 +76,8 @@ function buildElasticsearchTransport() {
73
76
  return new ElasticsearchTransport({
74
77
  level: config.logging.level,
75
78
  indexPrefix: config.logging.elasticsearch.index,
79
+ bufferLimit: 1000,
80
+ retryLimit: 5,
76
81
  clientOpts: {
77
82
  node: config.logging.elasticsearch.node,
78
83
  auth: config.logging.elasticsearch.username &&
@@ -117,8 +122,17 @@ function buildTransports() {
117
122
  return transports;
118
123
  }
119
124
  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(),
125
+ exports.winstonLogger = config.logging.enabled
126
+ ? winston.createLogger({
127
+ level: config.logging.level,
128
+ defaultMeta: {
129
+ service: config.serviceName,
130
+ environment: config.environment,
131
+ },
132
+ transports: buildTransports(),
133
+ })
134
+ :
135
+ winston.createLogger({ silent: true });
136
+ exports.winstonLogger.on('error', (err) => {
137
+ console.error('[apm] log transport error:', err instanceof Error ? err.message : err);
124
138
  });
package/dist/tracing.js CHANGED
@@ -1,19 +1,21 @@
1
1
  "use strict";
2
+ var _a, _b;
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  const apm_config_1 = require("./apm.config");
4
5
  const config = (0, apm_config_1.getApmConfig)();
5
6
  if (config.enabled && config.tracing.enabled && config.tracing.otlpEndpoint) {
7
+ if (!process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS) {
8
+ process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS =
9
+ 'http,undici,express,nestjs-core,pg,ioredis,redis,amqplib';
10
+ }
11
+ (_a = process.env).OTEL_METRICS_EXPORTER || (_a.OTEL_METRICS_EXPORTER = 'none');
12
+ (_b = process.env).OTEL_LOGS_EXPORTER || (_b.OTEL_LOGS_EXPORTER = 'none');
6
13
  const { NodeSDK } = require('@opentelemetry/sdk-node');
7
14
  const { getNodeAutoInstrumentations, } = require('@opentelemetry/auto-instrumentations-node');
8
- const { OTLPTraceExporter, } = require('@opentelemetry/exporter-trace-otlp-http');
9
15
  const { resourceFromAttributes } = require('@opentelemetry/resources');
10
16
  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 traceExporter = createTraceExporter();
18
+ const ignorePaths = config.http.ignorePaths;
17
19
  const sdk = new NodeSDK({
18
20
  resource: resourceFromAttributes({
19
21
  [ATTR_SERVICE_NAME]: config.serviceName,
@@ -24,20 +26,46 @@ if (config.enabled && config.tracing.enabled && config.tracing.otlpEndpoint) {
24
26
  instrumentations: [
25
27
  getNodeAutoInstrumentations({
26
28
  '@opentelemetry/instrumentation-fs': { enabled: false },
29
+ '@opentelemetry/instrumentation-http': {
30
+ ignoreIncomingRequestHook: (req) => ignorePaths.some((p) => { var _a; return (_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(p); }),
31
+ },
27
32
  }),
28
33
  ],
29
34
  });
30
35
  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
+ const SHUTDOWN_TIMEOUT_MS = 5000;
37
+ const onSignal = (signal) => {
38
+ let timer;
39
+ Promise.race([
40
+ sdk.shutdown(),
41
+ new Promise((resolve) => {
42
+ timer = setTimeout(resolve, SHUTDOWN_TIMEOUT_MS);
43
+ }),
44
+ ])
45
+ .catch((err) => console.error('[apm] error shutting down OTel SDK', err))
46
+ .finally(() => {
47
+ clearTimeout(timer);
48
+ if (process.listenerCount(signal) === 0) {
49
+ process.kill(process.pid, signal);
50
+ }
51
+ });
36
52
  };
37
- process.on('SIGTERM', shutdown);
38
- process.on('SIGINT', shutdown);
39
- console.info(`[apm] tracing enabled -> ${config.tracing.otlpEndpoint} (service: ${config.serviceName})`);
53
+ process.once('SIGTERM', onSignal);
54
+ process.once('SIGINT', onSignal);
55
+ console.info(`[apm] tracing enabled -> ${config.tracing.otlpEndpoint} (${config.tracing.protocol}, service: ${config.serviceName})`);
40
56
  }
41
57
  else {
42
58
  console.info('[apm] tracing disabled (set OTEL_EXPORTER_OTLP_ENDPOINT to enable)');
43
59
  }
60
+ function createTraceExporter() {
61
+ const { otlpEndpoint, protocol, headers } = config.tracing;
62
+ if (protocol === 'grpc') {
63
+ const { OTLPTraceExporter, } = require('@opentelemetry/exporter-trace-otlp-grpc');
64
+ return new OTLPTraceExporter({ url: otlpEndpoint });
65
+ }
66
+ const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
67
+ return new OTLPTraceExporter({
68
+ url: `${otlpEndpoint.replace(/\/$/, '')}/v1/traces`,
69
+ headers,
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlibs/apm",
3
- "version": "0.1.2",
3
+ "version": "1.1.0",
4
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
5
  "author": "QBIT Developer",
6
6
  "license": "MIT",
@@ -28,7 +28,9 @@
28
28
  "@opentelemetry/api": "^1.9.1",
29
29
  "@opentelemetry/api-logs": "^0.221.0",
30
30
  "@opentelemetry/auto-instrumentations-node": "^0.79.0",
31
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.221.0",
31
32
  "@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
33
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
32
34
  "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
33
35
  "@opentelemetry/resources": "^2.10.0",
34
36
  "@opentelemetry/sdk-logs": "^0.221.0",