@qlibs/apm 0.3.0 → 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.
@@ -78,12 +79,82 @@ dependency of this package.
78
79
  | `OTEL_EXPORTER_OTLP_HEADERS` | _(empty)_ | Comma-separated `key=value` pairs, e.g. ingestion keys / auth tokens. |
79
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. |
80
81
  | `APM_LOG_FORMAT` | `pretty` for `local`/`development`, `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
+ | `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. |
82
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. |
83
84
  | `APM_HTTP_IGNORE_PATHS` | `/health,/favicon.ico` | Comma list of path prefixes excluded from the HTTP access log and from tracing. |
84
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. |
85
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%). |
86
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
+
87
158
  ## Example configurations
88
159
 
89
160
  `LOG_LEVEL` is a threshold: it keeps that level and everything more severe
@@ -104,7 +175,22 @@ Errors are still marked on the trace span by `recordException()` regardless of
104
175
  `LOG_LEVEL`. After changing env vars, restart the app (pm2:
105
176
  `pm2 restart <app> --update-env`).
106
177
 
107
- **1. Production, minimal — only warn + error in pm2/docker, traces to SigNoz**
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**
108
194
  ```env
109
195
  NODE_ENV=production
110
196
  APM_SERVICE_NAME=my-app-api
@@ -115,7 +201,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
115
201
  Note: at `warn` the HTTP access log (`GET /api/... 200`, level `info`) is
116
202
  dropped too.
117
203
 
118
- **2. Production, standard — info + access log in pm2/docker, logs and traces to SigNoz**
204
+ **3. Production, standard — info + access log in pm2/docker, logs and traces to SigNoz**
119
205
  ```env
120
206
  NODE_ENV=production
121
207
  APM_SERVICE_NAME=my-app-api
@@ -124,7 +210,7 @@ APM_LOG_TRANSPORT=stdout,otlp
124
210
  OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
125
211
  ```
126
212
 
127
- **3. Production, high traffic — trace only 10% of requests**
213
+ **4. Production, high traffic — trace only 10% of requests**
128
214
  ```env
129
215
  NODE_ENV=production
130
216
  APM_SERVICE_NAME=my-app-api
@@ -135,7 +221,7 @@ OTEL_TRACES_SAMPLER=parentbased_traceidratio
135
221
  OTEL_TRACES_SAMPLER_ARG=0.1
136
222
  ```
137
223
 
138
- **4. Staging, investigating a bug — everything**
224
+ **5. Staging, investigating a bug — everything**
139
225
  ```env
140
226
  NODE_ENV=staging
141
227
  APM_SERVICE_NAME=my-app-api
@@ -144,7 +230,7 @@ APM_LOG_TRANSPORT=stdout,otlp
144
230
  OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4318
145
231
  ```
146
232
 
147
- **5. Local development, no SigNoz**
233
+ **6. Local development, no SigNoz**
148
234
  ```env
149
235
  NODE_ENV=local
150
236
  APM_SERVICE_NAME=my-app-api
@@ -152,7 +238,7 @@ APM_SERVICE_NAME=my-app-api
152
238
  # OTEL_EXPORTER_OTLP_ENDPOINT left empty -> tracing off
153
239
  ```
154
240
 
155
- **6. Logs to pm2/docker only, no tracing at all**
241
+ **7. Logs to pm2/docker only, no tracing at all**
156
242
  ```env
157
243
  NODE_ENV=production
158
244
  APM_SERVICE_NAME=my-app-api
@@ -161,13 +247,13 @@ APM_LOG_TRANSPORT=stdout
161
247
  # OTEL_EXPORTER_OTLP_ENDPOINT not set -> nothing is sent to SigNoz
162
248
  ```
163
249
 
164
- **7. Collector listening on gRPC (port 4317)**
250
+ **8. Collector listening on gRPC (port 4317)**
165
251
  ```env
166
252
  OTEL_EXPORTER_OTLP_ENDPOINT=http://signoz-otel-collector:4317
167
253
  OTEL_EXPORTER_OTLP_PROTOCOL=grpc
168
254
  ```
169
255
 
170
- **8. Logs to Elasticsearch / Kibana**
256
+ **9. Logs to Elasticsearch / Kibana**
171
257
  ```env
172
258
  NODE_ENV=production
173
259
  APM_SERVICE_NAME=my-app-api
@@ -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[];
@@ -45,6 +45,7 @@ function getApmConfig() {
45
45
  headers: parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS),
46
46
  },
47
47
  logging: {
48
+ enabled: !transports.includes('none'),
48
49
  level: env.LOG_LEVEL || (nodeEnv === 'local' ? 'debug' : 'info'),
49
50
  format: env.APM_LOG_FORMAT || (isLocal ? 'pretty' : 'json'),
50
51
  transports: transports.length ? transports : ['stdout'],
@@ -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();
@@ -1,5 +1,6 @@
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;
@@ -8,24 +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
  }
25
54
  fatal(message, context) {
55
+ if (this.fallback)
56
+ return this.fallback.fatal(message, context);
26
57
  winston_instance_1.winstonLogger.error(this.stringify(message), { context, fatal: true });
27
58
  }
28
59
  verbose(message, context) {
60
+ if (this.fallback)
61
+ return this.fallback.verbose(message, context);
29
62
  winston_instance_1.winstonLogger.verbose(this.stringify(message), { context });
30
63
  }
31
64
  stringify(message) {
@@ -122,11 +122,17 @@ function buildTransports() {
122
122
  return transports;
123
123
  }
124
124
  const config = (0, apm_config_1.getApmConfig)();
125
- exports.winstonLogger = winston.createLogger({
126
- level: config.logging.level,
127
- defaultMeta: { service: config.serviceName, environment: config.environment },
128
- transports: buildTransports(),
129
- });
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 });
130
136
  exports.winstonLogger.on('error', (err) => {
131
137
  console.error('[apm] log transport error:', err instanceof Error ? err.message : err);
132
138
  });
package/dist/tracing.js CHANGED
@@ -1,4 +1,5 @@
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)();
@@ -7,6 +8,8 @@ if (config.enabled && config.tracing.enabled && config.tracing.otlpEndpoint) {
7
8
  process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS =
8
9
  'http,undici,express,nestjs-core,pg,ioredis,redis,amqplib';
9
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');
10
13
  const { NodeSDK } = require('@opentelemetry/sdk-node');
11
14
  const { getNodeAutoInstrumentations, } = require('@opentelemetry/auto-instrumentations-node');
12
15
  const { resourceFromAttributes } = require('@opentelemetry/resources');
@@ -32,12 +35,16 @@ if (config.enabled && config.tracing.enabled && config.tracing.otlpEndpoint) {
32
35
  sdk.start();
33
36
  const SHUTDOWN_TIMEOUT_MS = 5000;
34
37
  const onSignal = (signal) => {
38
+ let timer;
35
39
  Promise.race([
36
40
  sdk.shutdown(),
37
- new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS).unref()),
41
+ new Promise((resolve) => {
42
+ timer = setTimeout(resolve, SHUTDOWN_TIMEOUT_MS);
43
+ }),
38
44
  ])
39
45
  .catch((err) => console.error('[apm] error shutting down OTel SDK', err))
40
46
  .finally(() => {
47
+ clearTimeout(timer);
41
48
  if (process.listenerCount(signal) === 0) {
42
49
  process.kill(process.pid, signal);
43
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlibs/apm",
3
- "version": "0.3.0",
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",