@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 +151 -0
- package/dist/apm.config.d.ts +30 -0
- package/dist/apm.config.js +65 -0
- package/dist/apm.module.d.ts +2 -0
- package/dist/apm.module.js +21 -0
- package/dist/filters/apm-exception.filter.d.ts +4 -0
- package/dist/filters/apm-exception.filter.js +33 -0
- package/dist/http/apm-logging.interceptor.d.ts +4 -0
- package/dist/http/apm-logging.interceptor.js +60 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/logger/apm-logger.service.d.ts +9 -0
- package/dist/logger/apm-logger.service.js +44 -0
- package/dist/logger/otlp-log.transport.d.ts +5 -0
- package/dist/logger/otlp-log.transport.js +102 -0
- package/dist/logger/trace-context.format.d.ts +2 -0
- package/dist/logger/trace-context.format.js +47 -0
- package/dist/logger/winston.instance.d.ts +2 -0
- package/dist/logger/winston.instance.js +124 -0
- package/dist/record-exception.d.ts +1 -0
- package/dist/record-exception.js +18 -0
- package/dist/tracing.d.ts +1 -0
- package/dist/tracing.js +43 -0
- package/package.json +59 -0
- package/tracing.d.ts +2 -0
- package/tracing.js +4 -0
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,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,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,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);
|
package/dist/index.d.ts
ADDED
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,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,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,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 {};
|
package/dist/tracing.js
ADDED
|
@@ -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
package/tracing.js
ADDED