@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 +201 -8
- package/dist/apm.config.d.ts +2 -1
- package/dist/apm.config.js +4 -3
- package/dist/filters/apm-exception.filter.js +1 -1
- package/dist/http/apm-logging.interceptor.js +3 -3
- package/dist/logger/apm-logger.service.d.ts +2 -0
- package/dist/logger/apm-logger.service.js +36 -0
- package/dist/logger/otlp-log.transport.js +20 -8
- package/dist/logger/winston.instance.js +19 -5
- package/dist/tracing.js +43 -15
- package/package.json +3 -1
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.
|
|
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` |
|
|
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`
|
|
80
|
-
| `APM_LOG_FORMAT` | `pretty`
|
|
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
|
|
package/dist/apm.config.d.ts
CHANGED
|
@@ -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[];
|
package/dist/apm.config.js
CHANGED
|
@@ -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 || '
|
|
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.
|
|
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.
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 (
|
|
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({
|
|
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 =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 =
|
|
12
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
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.
|
|
38
|
-
process.
|
|
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": "
|
|
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",
|