Point3 서비스용 OpenTelemetry SDK 래퍼. import 한 줄로 traces, metrics, logs 수집이 시작된다.
npm install @point3/observability@opentelemetry/api는 peer dependency로 자동 설치된다 (npm v7+).
main.ts 최상단에 side-effect import 추가. 반드시 dotenv/config 다음, 모든 애플리케이션 import 전에 위치해야 한다. OTel의 monkey-patching이 모듈 로드 전에 적용되어야 하기 때문.
import "dotenv/config";
import "@point3/observability";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";서비스명은 package.json의 name 필드에서 자동으로 읽는다. 별도 설정 불필요.
instrumentation 추가, exporter 교체 등이 필요한 경우 register() 함수를 사용한다. ESM import hoisting 때문에 반드시 별도 파일로 분리해야 한다.
// src/observability.ts
import { register } from "@point3/observability/register";
register({
instrumentations: (defaults) => [...defaults, new PrismaInstrumentation()],
});// src/main.ts
import "dotenv/config";
import "./observability";
import { NestFactory } from "@nestjs/core";register() 호출 시 console.log, console.error 등 표준 콘솔 메서드가 자동으로 패치된다. 패치된 console은 두 가지 일을 동시에 수행한다:
- stdout/stderr 출력 — 터미널에서 로그를 바로 확인할 수 있다
- OTel LogRecord 전송 — OTLP를 통해 Alloy → Loki로 수집된다
active span이 있으면 trace_id와 span_id가 자동으로 LogRecord에 주입되어, Loki에서 Tempo 트레이스로 바로 연결할 수 있다.
| console 메서드 | 로그 레벨 | 출력 대상 |
|---|---|---|
console.log() |
info |
stdout |
console.info() |
info |
stdout |
console.warn() |
warn |
stdout |
console.error() |
error |
stderr |
console.debug() |
debug |
stdout (기본 LOG_LEVEL=info에서 필터됨) |
첫 번째 인자가 객체면 message 필드를 로그 메시지로, 나머지 필드를 OTel LogRecord attributes로 자동 추출한다.
// 단순 문자열
console.log("서버 시작됨");
// 구조화 로깅 — orderId, amount가 OTel attributes로 추출됨
console.log({ message: "주문 생성", orderId: "123", amount: 50000 });
// Error 객체 — exception.stacktrace, exception.type이 자동 추가됨
console.error(new Error("결제 실패"));LOG_LEVEL 환경변수로 출력할 최소 레벨을 지정한다. 기본값은 info.
# error, fatal만 출력
LOG_LEVEL=error node main.js
# debug 이상 전부 출력
LOG_LEVEL=debug node main.js레벨 우선순위: fatal < error < warn < info < debug < verbose
OTel SDK는 사용하되 console 몽키패치를 원하지 않는 경우:
import { register } from "@point3/observability/register";
register({ patchConsole: false });console 패치 대신 직접 로깅을 제어해야 하는 경우 (예: 컴플라이언스 요구사항으로 PII 로그를 별도 파이프라인으로 분리), 필요 시 emitLog를 직접 사용할 수 있다.
import { register } from "@point3/observability/register";
import { emitLog } from "@point3/observability/log";
import type { LogLevel } from "@point3/observability/log";
register({ patchConsole: false }); // console 자동 패치 비활성화
// 관측 로그 → OTLP → Loki
emitLog("info", ["이체 요청 수신", { requestId: "abc" }]);
// PII 로그 → 별도 처리 (emitLog를 호출하지 않으므로 Loki에 안 감)
auditLogger.write({ accountNumber: "...", amount: 50000 });@point3/observability가 전송하는 OTLP 로그를 Loki로 라우팅하기 위해 Alloy 설정(config.alloy)을 수정한다.
기존 config에서 3줄만 추가하면 된다:
- receiver output에
logs추가 - batch processor output에
logs추가 otelcol.exporter.loki컴포넌트 추가
// ─── OTLP 수신 ───
otelcol.receiver.otlp "app_service" {
grpc {
endpoint = "0.0.0.0:4317"
}
http {
endpoint = "0.0.0.0:4318"
}
output {
metrics = [otelcol.processor.batch.default.input]
traces = [otelcol.processor.batch.default.input]
logs = [otelcol.processor.batch.default.input]
}
}
// ─── 배치 처리 ───
otelcol.processor.batch "default" {
output {
metrics = [otelcol.exporter.prometheus.app_service.input]
traces = [otelcol.exporter.otlp.tempo.input]
logs = [otelcol.exporter.loki.default.input]
}
}
// ─── Metrics → Prometheus ───
otelcol.exporter.prometheus "app_service" {
forward_to = [prometheus.remote_write.default.receiver]
}
// ─── Traces → Tempo ───
otelcol.exporter.otlp "tempo" {
client {
endpoint = "<TEMPO_HOST>:4317"
tls {
insecure = true
}
}
}
otelcol.exporter.loki "default" {
forward_to = [loki.write.default.receiver]
}
prometheus.remote_write "default" {
endpoint {
url = "http://<PROMETHEUS_HOST>:9090/api/v1/write"
}
}
loki.write "default" {
endpoint {
url = "http://<LOKI_HOST>:3100/loki/api/v1/push"
}
}| # | 위치 | 변경 |
|---|---|---|
| 1 | otelcol.receiver.otlp output |
logs = [otelcol.processor.batch.default.input] 추가 |
| 2 | otelcol.processor.batch output |
logs = [otelcol.exporter.loki.default.input] 추가 |
| 3 | 새 컴포넌트 | otelcol.exporter.loki "default" → 기존 loki.write.default 재사용 |
otelcol.exporter.loki는 OTel 리소스 속성 service.name을 Loki 레이블 service_name으로 자동 변환한다. 별도 transform 프로세서가 필요 없다.
| export | 설명 |
|---|---|
emitLog(level, args) |
OTel LogRecord 전송 + stdout/stderr 출력 |
shouldLog(level) |
LOG_LEVEL 기준 해당 레벨 출력 여부 |
getConfiguredLogLevel() |
현재 설정된 로그 레벨 반환 |
getOriginalConsole() |
패치 이전의 원본 console 메서드 |
patchConsole() / unpatchConsole() |
console 패치 수동 제어 |
LogLevel, LogEntry |
타입 |
| 변수 | 설명 | 기본값 |
|---|---|---|
OTEL_SERVICE_NAME |
서비스 이름 (최우선) | package.json name |
OTEL_SERVICE_VERSION |
서비스 버전 | package.json version |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP 수신 엔드포인트 | http://alloy:4318 |
LOG_LEVEL |
로그 출력 레벨 (error, warn, info, debug, verbose) | info |
NODE_ENV |
실행 환경 | — |
설정 우선순위: 환경변수 > register() 옵션 > package.json > 기본값
설정 없이 import만 하면 아래가 자동으로 구성된다:
- Exporters: OTLP HTTP (traces, metrics, logs) →
http://alloy:4318 - Instrumentations:
getNodeAutoInstrumentations()+RuntimeNodeInstrumentation - Metric 전송 간격: 2000ms
- 런타임 계측 정밀도: 2000ms
- Graceful Shutdown:
SIGINT,SIGTERM시 SDK 종료 후process.exit()
| import 경로 | 동작 |
|---|---|
@point3/observability |
side-effect — import 시 즉시 SDK 시작 |
@point3/observability/register |
register() 함수만 export, side-effect 없음 |
@point3/observability/log |
emitLog, patchConsole 등 로그 유틸리티 export |
@point3/observability를 import하면 내부적으로 register()를 옵션 없이 호출한다. 커스텀이 필요하면 @point3/observability/register에서 직접 호출.
npm run build # tsup으로 ESM + CommonJS 듀얼 빌드출력: dist/ — .js(ESM), .cjs(CJS), .d.ts(타입), .map(소스맵)