@point3/observability 0.2.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 +383 -0
- package/dist/index.cjs +346 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +344 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/index.cjs +274 -0
- package/dist/nest/index.cjs.map +1 -0
- package/dist/nest/index.d.cts +19 -0
- package/dist/nest/index.d.ts +19 -0
- package/dist/nest/index.js +272 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/register.cjs +345 -0
- package/dist/register.cjs.map +1 -0
- package/dist/register.d.cts +33 -0
- package/dist/register.d.ts +33 -0
- package/dist/register.js +343 -0
- package/dist/register.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# @point3/observability
|
|
2
|
+
|
|
3
|
+
Point3 서비스용 OpenTelemetry SDK 래퍼. import 한 줄로 traces, metrics, logs 수집이 시작된다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @point3/observability
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@opentelemetry/api`는 peer dependency로 자동 설치된다 (npm v7+).
|
|
12
|
+
|
|
13
|
+
## 사용법
|
|
14
|
+
|
|
15
|
+
### 기본 (제로 설정)
|
|
16
|
+
|
|
17
|
+
`main.ts` 최상단에 side-effect import 추가. 반드시 `dotenv/config` 다음, 모든 애플리케이션 import 전에 위치해야 한다. OTel의 monkey-patching이 모듈 로드 전에 적용되어야 하기 때문.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import "dotenv/config";
|
|
21
|
+
import "@point3/observability";
|
|
22
|
+
|
|
23
|
+
import { NestFactory } from "@nestjs/core";
|
|
24
|
+
import { AppModule } from "./app.module";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
서비스명은 `package.json`의 `name` 필드에서 자동으로 읽는다. 별도 설정 불필요.
|
|
28
|
+
|
|
29
|
+
`package.json` 탐색은 `require.main.filename` → `process.argv[1]` → `process.cwd()` 순서로 시작 디렉토리를 결정한 뒤, 상위로 올라가며 가장 가까운 `package.json`을 찾는다. 어디서 `node`를 실행하든 정확한 프로젝트 루트를 찾을 수 있다.
|
|
30
|
+
|
|
31
|
+
### 커스텀 설정
|
|
32
|
+
|
|
33
|
+
instrumentation 추가, exporter 교체 등이 필요한 경우 `register()` 함수를 사용한다. **ESM import hoisting 때문에 반드시 별도 파일로 분리해야 한다.**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// src/observability.ts
|
|
37
|
+
import { register } from "@point3/observability/register";
|
|
38
|
+
|
|
39
|
+
register({
|
|
40
|
+
instrumentations: (defaults) => [...defaults, new PrismaInstrumentation()],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// src/main.ts
|
|
46
|
+
import "dotenv/config";
|
|
47
|
+
import "./observability";
|
|
48
|
+
|
|
49
|
+
import { NestFactory } from "@nestjs/core";
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 로깅
|
|
53
|
+
|
|
54
|
+
`register()` 호출 시 `console.log`, `console.error` 등 표준 콘솔 메서드가 자동으로 패치된다. 패치된 console은 두 가지 일을 동시에 수행한다:
|
|
55
|
+
|
|
56
|
+
1. **stdout/stderr 출력** — 터미널에서 로그를 바로 확인할 수 있다
|
|
57
|
+
2. **OTel LogRecord 전송** — OTLP를 통해 Alloy → Loki로 수집된다
|
|
58
|
+
|
|
59
|
+
active span이 있으면 `trace_id`와 `span_id`가 자동으로 LogRecord에 주입되어, Loki에서 Tempo 트레이스로 바로 연결할 수 있다.
|
|
60
|
+
|
|
61
|
+
### console 메서드 → 로그 레벨 매핑
|
|
62
|
+
|
|
63
|
+
| console 메서드 | 로그 레벨 | 출력 대상 |
|
|
64
|
+
|---------------|----------|----------|
|
|
65
|
+
| `console.log()` | `info` | stdout |
|
|
66
|
+
| `console.info()` | `info` | stdout |
|
|
67
|
+
| `console.warn()` | `warn` | stdout |
|
|
68
|
+
| `console.error()` | `error` | stderr |
|
|
69
|
+
| `console.debug()` | `debug` | stdout (기본 LOG_LEVEL=info에서 필터됨) |
|
|
70
|
+
|
|
71
|
+
### stdout 출력 포맷
|
|
72
|
+
|
|
73
|
+
`NODE_ENV`에 따라 출력 포맷이 달라진다.
|
|
74
|
+
|
|
75
|
+
**개발 환경** (`NODE_ENV !== 'production'`) — Pretty Print:
|
|
76
|
+
```
|
|
77
|
+
INFO 2026-02-13T08:30:00.000Z 주문 생성 완료 trace_id=4bf92f3577b34da6
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**프로덕션** (`NODE_ENV=production`) — JSON:
|
|
81
|
+
```json
|
|
82
|
+
{"level":"info","message":"주문 생성 완료","timestamp":"2026-02-13T08:30:00.000Z","trace_id":"4bf92f3577b34da6","span_id":"00f067aa0ba902b7"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 구조화 로깅
|
|
86
|
+
|
|
87
|
+
첫 번째 인자가 객체면 `message` 필드를 로그 메시지로, 나머지 필드를 OTel LogRecord attributes로 자동 추출한다.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// 단순 문자열
|
|
91
|
+
console.log("서버 시작됨");
|
|
92
|
+
|
|
93
|
+
// 구조화 로깅 — orderId, amount가 OTel attributes로 추출됨
|
|
94
|
+
console.log({ message: "주문 생성", orderId: "123", amount: 50000 });
|
|
95
|
+
|
|
96
|
+
// Error 객체 — exception.stacktrace, exception.type이 자동 추가됨
|
|
97
|
+
console.error(new Error("결제 실패"));
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 레벨 필터링
|
|
101
|
+
|
|
102
|
+
`LOG_LEVEL` 환경변수로 출력할 최소 레벨을 지정한다. 기본값은 `info`.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# error, fatal만 출력
|
|
106
|
+
LOG_LEVEL=error node main.js
|
|
107
|
+
|
|
108
|
+
# debug 이상 전부 출력
|
|
109
|
+
LOG_LEVEL=debug node main.js
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
레벨 우선순위: `fatal` < `error` < `warn` < `info` < `debug` < `verbose`
|
|
113
|
+
|
|
114
|
+
### 콘솔 패치 비활성화
|
|
115
|
+
|
|
116
|
+
OTel SDK는 사용하되 console 몽키패치를 원하지 않는 경우:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { register } from "@point3/observability/register";
|
|
120
|
+
|
|
121
|
+
register({ patchConsole: false });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## NestJS 통합
|
|
125
|
+
|
|
126
|
+
NestJS 애플리케이션에서 전용 로거 모듈을 사용하여 OTel 로그와 통합할 수 있다.
|
|
127
|
+
|
|
128
|
+
### Point3LoggerModule 설정
|
|
129
|
+
|
|
130
|
+
`AppModule`에서 `Point3LoggerModule`을 import 한다.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { Module } from "@nestjs/common";
|
|
134
|
+
import { Point3LoggerModule } from "@point3/observability/nest";
|
|
135
|
+
|
|
136
|
+
@Module({
|
|
137
|
+
imports: [Point3LoggerModule],
|
|
138
|
+
})
|
|
139
|
+
export class AppModule {}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 전역 로거 적용
|
|
143
|
+
|
|
144
|
+
`main.ts`에서 `app.useLogger()`를 사용하여 NestJS 시스템 로그를 `Point3Logger`로 교체한다.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { NestFactory } from "@nestjs/core";
|
|
148
|
+
import { Point3Logger } from "@point3/observability/nest";
|
|
149
|
+
import { AppModule } from "./app.module";
|
|
150
|
+
|
|
151
|
+
async function bootstrap() {
|
|
152
|
+
const app = await NestFactory.create(AppModule);
|
|
153
|
+
|
|
154
|
+
// NestJS 내부 로그를 Point3Logger로 출력
|
|
155
|
+
app.useLogger(app.get(Point3Logger));
|
|
156
|
+
|
|
157
|
+
await app.listen(3000);
|
|
158
|
+
}
|
|
159
|
+
bootstrap();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### DI 주입 및 사용
|
|
163
|
+
|
|
164
|
+
서비스나 컨트롤러에서 `Point3Logger`를 주입받아 사용할 수 있다.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { Injectable } from "@nestjs/common";
|
|
168
|
+
import { Point3Logger } from "@point3/observability/nest";
|
|
169
|
+
|
|
170
|
+
@Injectable()
|
|
171
|
+
export class AppService {
|
|
172
|
+
constructor(private readonly logger: Point3Logger) {
|
|
173
|
+
this.logger.setContext(AppService.name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
doSomething() {
|
|
177
|
+
this.logger.log("작업 수행 중...", { detail: "extra info" });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Winston 마이그레이션
|
|
183
|
+
|
|
184
|
+
기존 `nest-winston` 등을 사용하던 환경에서 쉽게 전환할 수 있다.
|
|
185
|
+
|
|
186
|
+
**Before:**
|
|
187
|
+
```typescript
|
|
188
|
+
import { WinstonModule } from "nest-winston";
|
|
189
|
+
// ... winston 설정 복잡함
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**After:**
|
|
193
|
+
```typescript
|
|
194
|
+
import { Point3LoggerModule } from "@point3/observability/nest";
|
|
195
|
+
|
|
196
|
+
@Module({
|
|
197
|
+
imports: [Point3LoggerModule],
|
|
198
|
+
})
|
|
199
|
+
export class AppModule {}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Alloy 설정
|
|
203
|
+
|
|
204
|
+
`@point3/observability`가 전송하는 OTLP 로그를 Loki로 라우팅하기 위해 Alloy 설정(`config.alloy`)을 수정한다.
|
|
205
|
+
|
|
206
|
+
기존 config에서 **3줄만 추가**하면 된다:
|
|
207
|
+
|
|
208
|
+
1. receiver output에 `logs` 추가
|
|
209
|
+
2. batch processor output에 `logs` 추가
|
|
210
|
+
3. `otelcol.exporter.loki` 컴포넌트 추가
|
|
211
|
+
|
|
212
|
+
### 전체 표준 config
|
|
213
|
+
|
|
214
|
+
`// [NEW]` 주석이 붙은 라인이 추가분이다. 나머지는 기존과 동일.
|
|
215
|
+
|
|
216
|
+
```alloy
|
|
217
|
+
// ─── OTLP 수신 ───
|
|
218
|
+
otelcol.receiver.otlp "app_service" {
|
|
219
|
+
grpc {
|
|
220
|
+
endpoint = "0.0.0.0:4137"
|
|
221
|
+
}
|
|
222
|
+
http {
|
|
223
|
+
endpoint = "0.0.0.0:4138"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
output {
|
|
227
|
+
metrics = [otelcol.processor.batch.default.input]
|
|
228
|
+
traces = [otelcol.processor.batch.default.input]
|
|
229
|
+
logs = [otelcol.processor.batch.default.input] // [NEW] OTLP 로그 라우팅
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── 배치 처리 ───
|
|
234
|
+
otelcol.processor.batch "default" {
|
|
235
|
+
output {
|
|
236
|
+
metrics = [otelcol.exporter.prometheus.app_service.input]
|
|
237
|
+
traces = [otelcol.exporter.otlp.tempo.input]
|
|
238
|
+
logs = [otelcol.exporter.loki.default.input] // [NEW] 로그 → Loki
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Metrics → Prometheus ───
|
|
243
|
+
otelcol.exporter.prometheus "app_service" {
|
|
244
|
+
forward_to = [prometheus.remote_write.default.receiver]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Traces → Tempo ───
|
|
248
|
+
otelcol.exporter.otlp "tempo" {
|
|
249
|
+
client {
|
|
250
|
+
endpoint = "<TEMPO_HOST>:4317"
|
|
251
|
+
tls {
|
|
252
|
+
insecure = true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Logs → Loki ─── [NEW]
|
|
258
|
+
otelcol.exporter.loki "default" {
|
|
259
|
+
forward_to = [loki.write.default.receiver]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
prometheus.remote_write "default" {
|
|
263
|
+
endpoint {
|
|
264
|
+
url = "http://<PROMETHEUS_HOST>:9090/api/v1/write"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
loki.write "default" {
|
|
269
|
+
endpoint {
|
|
270
|
+
url = "http://<LOKI_HOST>:3100/loki/api/v1/push"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 변경 요약
|
|
276
|
+
|
|
277
|
+
| # | 위치 | 변경 |
|
|
278
|
+
|---|------|------|
|
|
279
|
+
| 1 | `otelcol.receiver.otlp` output | `logs = [otelcol.processor.batch.default.input]` 추가 |
|
|
280
|
+
| 2 | `otelcol.processor.batch` output | `logs = [otelcol.exporter.loki.default.input]` 추가 |
|
|
281
|
+
| 3 | 새 컴포넌트 | `otelcol.exporter.loki "default"` → 기존 `loki.write.default` 재사용 |
|
|
282
|
+
|
|
283
|
+
`otelcol.exporter.loki`는 OTel 리소스 속성 `service.name`을 Loki 레이블 `service_name`으로 자동 변환한다. 별도 transform 프로세서가 필요 없다.
|
|
284
|
+
|
|
285
|
+
### Docker 컨테이너 로그 수집이 필요한 경우
|
|
286
|
+
|
|
287
|
+
OTLP로 앱 로그가 직접 Loki에 전송되므로 Docker socket 스크래핑은 기본 config에 포함하지 않는다. DB, Redis 등 비계측 컨테이너의 로그도 Loki에서 보려면 아래를 추가한다.
|
|
288
|
+
|
|
289
|
+
```alloy
|
|
290
|
+
discovery.docker "local" {
|
|
291
|
+
host = "unix:///var/run/docker.sock"
|
|
292
|
+
filter {
|
|
293
|
+
name = "label"
|
|
294
|
+
values = ["com.docker.compose.project=" + env("COMPOSE_PROJECT_NAME")]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
discovery.relabel "local" {
|
|
299
|
+
targets = discovery.docker.local.targets
|
|
300
|
+
|
|
301
|
+
rule {
|
|
302
|
+
source_labels = ["__meta_docker_container_name"]
|
|
303
|
+
target_label = "container"
|
|
304
|
+
regex = "/(.*)"
|
|
305
|
+
}
|
|
306
|
+
rule {
|
|
307
|
+
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
|
|
308
|
+
target_label = "service"
|
|
309
|
+
}
|
|
310
|
+
rule {
|
|
311
|
+
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
|
|
312
|
+
target_label = "project"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
loki.source.docker "local" {
|
|
317
|
+
host = "unix:///var/run/docker.sock"
|
|
318
|
+
targets = discovery.relabel.local.output
|
|
319
|
+
forward_to = [loki.write.default.receiver]
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
> ⚠️ 앱 컨테이너의 stdout도 수집되므로 OTLP 로그와 중복될 수 있다.
|
|
324
|
+
|
|
325
|
+
### Grafana Loki 데이터소스 설정
|
|
326
|
+
|
|
327
|
+
Loki에서 Tempo(Trace)로 바로 이동할 수 있도록 **Derived Fields**를 설정한다.
|
|
328
|
+
|
|
329
|
+
- **Name**: `trace_id`
|
|
330
|
+
- **Regex**: `traceID=(\w+)` (OTLP 로그)
|
|
331
|
+
- **Internal link**: Tempo 데이터소스 선택, Query에 `${__value.raw}` 입력
|
|
332
|
+
|
|
333
|
+
## 환경변수
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
| 변수 | 설명 | 기본값 |
|
|
337
|
+
|------|------|--------|
|
|
338
|
+
| `OTEL_SERVICE_NAME` | 서비스 이름 (최우선) | `package.json` name |
|
|
339
|
+
| `OTEL_SERVICE_VERSION` | 서비스 버전 | `package.json` version |
|
|
340
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP 수신 엔드포인트 | `http://alloy:4318` |
|
|
341
|
+
| `LOG_LEVEL` | 로그 출력 레벨 (error, warn, info, debug, verbose) | `info` |
|
|
342
|
+
| `NODE_ENV` | 실행 환경 (production일 경우 JSON 로깅 활성화) | `development` |
|
|
343
|
+
|
|
344
|
+
설정 우선순위: **환경변수 > `register()` 옵션 > `package.json` > 기본값**
|
|
345
|
+
|
|
346
|
+
## 기본 동작
|
|
347
|
+
|
|
348
|
+
설정 없이 import만 하면 아래가 자동으로 구성된다:
|
|
349
|
+
|
|
350
|
+
- **Exporters**: OTLP HTTP (traces, metrics, logs) → `http://alloy:4318`
|
|
351
|
+
- **Instrumentations**: `getNodeAutoInstrumentations()` + `RuntimeNodeInstrumentation`
|
|
352
|
+
- **Metric 전송 간격**: 2000ms
|
|
353
|
+
- **런타임 계측 정밀도**: 2000ms
|
|
354
|
+
- **Graceful Shutdown**: `SIGINT`, `SIGTERM` 시 SDK 종료 후 `process.exit()`
|
|
355
|
+
|
|
356
|
+
## 진입점
|
|
357
|
+
|
|
358
|
+
| import 경로 | 동작 |
|
|
359
|
+
|------------|------|
|
|
360
|
+
| `@point3/observability` | side-effect — import 시 즉시 SDK 시작 |
|
|
361
|
+
| `@point3/observability/register` | `register()` 함수만 export, side-effect 없음 |
|
|
362
|
+
| `@point3/observability/nest` | NestJS 통합용 모듈 및 로거 export |
|
|
363
|
+
|
|
364
|
+
`@point3/observability`를 import하면 내부적으로 `register()`를 옵션 없이 호출한다. 커스텀이 필요하면 `@point3/observability/register`에서 직접 호출.
|
|
365
|
+
|
|
366
|
+
## 프로젝트 구조
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
src/
|
|
370
|
+
├── index.ts # side-effect 엔트리: register()를 옵션 없이 호출
|
|
371
|
+
├── register.ts # register() 구현 + export
|
|
372
|
+
├── types.ts # ObservabilityOptions 인터페이스
|
|
373
|
+
├── log/ # 로깅 처리 및 콘솔 패치 로직
|
|
374
|
+
└── nest/ # NestJS 통합 모듈 및 로거
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## 빌드
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npm run build # tsup으로 ESM + CJS 듀얼 빌드
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
출력: `dist/` — `.js`(ESM), `.cjs`(CJS), `.d.ts`(타입), `.map`(소스맵)
|