mega-framework 0.1.7 → 0.1.9
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 +9 -0
- package/package.json +3 -3
- package/sample/crud/.env +9 -0
- package/sample/crud/.env.example +9 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +28 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/routes/upload.js +20 -1
- package/sample/crud/apps/main/services/guide-service.js +4 -3
- package/sample/crud/apps/main/services/upload-demo-service.js +6 -5
- package/sample/crud/apps/main/views/upload/index.ejs +4 -1
- package/sample/crud/docs/guide/01-cli.md +587 -0
- package/sample/crud/docs/guide/02-router-controller.md +497 -0
- package/sample/crud/docs/guide/03-service-model-db.md +929 -0
- package/sample/crud/docs/guide/04-websocket-asp.md +632 -0
- package/sample/crud/docs/guide/05-scheduler-job-worker.md +400 -0
- package/sample/crud/docs/guide/06-config-auth-session-security.md +495 -0
- package/sample/crud/docs/guide/07-view-i18n-static-multipart.md +462 -0
- package/sample/crud/docs/guide/08-observability.md +373 -0
- package/sample/crud/mega.config.js +7 -0
- package/sample/crud/package.json +2 -2
- package/sample/crud/scripts/start-ws-hub.sh +18 -4
- package/sample/crud/var/uploads/(/355/212/271/352/270/260/354/236/220) 2026 /353/251/264/354/240/221/354/225/210/353/202/264-mqbnwq5v-d2125aa8.txt" +1 -0
- package/sample/crud/var/uploads/(/355/212/271/352/270/260/354/236/220) 2026 /353/251/264/354/240/221/354/225/210/353/202/264-mqbo0nbf-842b6135.txt" +1 -0
- package/sample/crud/var/uploads/00_b-mqbnh7lc-c9790ab8.png +0 -0
- package/sample/crud/var/uploads/2025-07-22 13 35 56-mqbngvvk-e545e90e.png +0 -0
- package/sample/crud/var/uploads/big-file-mqbo1h9e-2957eaf5.png +0 -0
- package/sample/crud/var/uploads//341/204/200/341/205/247/341/206/274/341/204/200/341/205/265/341/204/211/341/205/265/341/206/257/341/204/214/341/205/245/341/206/250/341/204/214/341/205/263/341/206/274/341/204/206/341/205/247/341/206/274/341/204/211/341/205/245-mqbo5yxh-5288d8ef.pdf +0 -0
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/adapters/adapter-options.js +14 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +7 -4
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +4 -1
- package/src/adapters/mongo-adapter.js +21 -7
- package/src/adapters/postgres-adapter.js +8 -4
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +6 -2
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/scaffold.js +38 -2
- package/src/cli/generators/index.js +58 -1
- package/src/cli/index.js +88 -59
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +3 -1
- package/src/core/ctx-builder.js +59 -1
- package/src/core/envelope.js +9 -2
- package/src/core/hub-link.js +24 -14
- package/src/core/index.js +1 -1
- package/src/core/mega-app.js +55 -45
- package/src/core/pipeline.js +8 -6
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +3 -3
- package/src/core/session-store.js +14 -1
- package/src/core/ws-presence.js +17 -5
- package/src/core/ws-roster.js +49 -10
- package/src/core/ws-upgrade.js +105 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +10 -0
- package/src/lib/mega-job-queue.js +53 -13
- package/src/lib/mega-job.js +8 -1
- package/src/lib/mega-metrics.js +28 -1
- package/src/lib/mega-plugin.js +2 -2
- package/src/lib/mega-worker.js +28 -5
- package/src/lib/ws-hub.js +90 -9
- package/templates/adr/code.tpl +23 -0
- package/types/adapters/adapter-options.d.ts +2 -0
- package/types/adapters/file-adapter.d.ts +12 -1
- package/types/adapters/file-session-adapter.d.ts +4 -2
- package/types/adapters/maria-adapter.d.ts +5 -3
- package/types/adapters/mega-cache-adapter.d.ts +27 -1
- package/types/adapters/mega-db-adapter.d.ts +4 -1
- package/types/adapters/mongo-adapter.d.ts +13 -2
- package/types/adapters/postgres-adapter.d.ts +4 -2
- package/types/adapters/redis-adapter.d.ts +8 -0
- package/types/adapters/sqlite-adapter.d.ts +8 -2
- package/types/cli/generators/index.d.ts +11 -1
- package/types/cli/index.d.ts +12 -27
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ctx-builder.d.ts +23 -0
- package/types/core/hub-link.d.ts +3 -1
- package/types/core/index.d.ts +1 -1
- package/types/core/mega-app.d.ts +1 -1
- package/types/core/pipeline.d.ts +2 -1
- package/types/core/security.d.ts +3 -3
- package/types/core/session-store.d.ts +7 -0
- package/types/core/ws-roster.d.ts +13 -1
- package/types/core/ws-upgrade.d.ts +29 -0
- package/types/lib/mega-circuit-breaker.d.ts +4 -2
- package/types/lib/mega-health.d.ts +7 -0
- package/types/lib/mega-job-queue.d.ts +16 -4
- package/types/lib/mega-job.d.ts +8 -1
- package/types/lib/mega-plugin.d.ts +1 -1
- package/types/lib/mega-worker.d.ts +3 -1
- package/types/lib/ws-hub.d.ts +27 -2
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# 관측 (Metrics + Swagger + Tracing + Logger)
|
|
2
|
+
|
|
3
|
+
서버가 "지금 잘 돌고 있는가"를 **밖에서 들여다보는** 네 가지 도구를 다룬다.
|
|
4
|
+
|
|
5
|
+
- **Metrics** — 숫자 집계(요청 초당 몇 건, 평균 얼마 걸림). Prometheus 가 긁어간다.
|
|
6
|
+
- **Swagger/OpenAPI** — API 설명서를 자동 생성해 `/docs` 웹페이지로 보여준다.
|
|
7
|
+
- **Tracing** — 요청 하나가 어디를 거쳐갔는지 분산 추적(span 트리). Zipkin/OTLP collector 로 보낸다.
|
|
8
|
+
- **Logger** — 구조적 JSON 로그(pino). 콘솔·파일·텔레그램으로 동시 출력.
|
|
9
|
+
|
|
10
|
+
네 가지 모두 **옵트인**이다. 켜지 않으면 코드 경로가 아예 안 붙어 **성능 비용 0**이다. 이 설계가 관측 모듈의 공통 뼈대다.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Metrics (Prometheus)
|
|
15
|
+
|
|
16
|
+
> 소스: `src/lib/mega-metrics.js`, `src/core/cluster-metrics.js`, 라우트 배선 `src/core/mega-app.js`, boot 배선 `src/core/boot.js`. (ADR-131 / ADR-132 / ADR-170)
|
|
17
|
+
|
|
18
|
+
### 1.1 무엇인가
|
|
19
|
+
|
|
20
|
+
트레이싱이 "요청 하나가 어디를 거쳐갔나"를 **개별 추적**한다면, 메트릭은 "요청이 초당 몇 건 왔고 평균 얼마 걸렸나"를 **숫자로 집계**한다. 운영자는 이 집계 숫자를 Prometheus 로 scrape 해 대시보드·알람을 만든다.
|
|
21
|
+
|
|
22
|
+
내부적으로 OpenTelemetry metrics SDK 를 쓰되, `PrometheusExporter` 를 `preventServerStart: true` 로 띄워 **자체 :9464 서버를 끄고** MetricReader 로만 쓴다. `/metrics` 라우트가 `reader.collect()` → `PrometheusSerializer` 로 텍스트를 만들어 **메인 포트로** 응답한다(`/health` 면제 패턴 정합).
|
|
23
|
+
|
|
24
|
+
### 1.2 켜기
|
|
25
|
+
|
|
26
|
+
메트릭은 `mega.config.js` 의 `health.exposeMetrics: true` 로 켠다. 그러면 boot 가 `MegaMetrics.init()` 을 호출하고, 모든 공유 어댑터의 `onCallEnd` 에 일괄 구독한다.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
// mega.config.js
|
|
30
|
+
export default {
|
|
31
|
+
health: {
|
|
32
|
+
exposeMetrics: true, // /metrics 라우트 등록 + SDK 초기화
|
|
33
|
+
metricsPath: '/metrics', // 디폴트 '/metrics'
|
|
34
|
+
metricsAllowList: ['127.0.0.1', '::1'], // 접근 허용 IP/CIDR (빈 배열이면 전체 노출)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> boot 배선(`src/core/boot.js`): `health.exposeMetrics === true` 일 때만 `MegaMetrics.init({ serviceName, version, environment })` + `attachToManager`. serviceName 은 `health.serviceName` → `server.serviceName` → `MEGA_OTEL_SERVICE_NAME` → `'mega-framework'` 순으로 폴백한다.
|
|
40
|
+
|
|
41
|
+
### 1.3 `/metrics` 엔드포인트 (IP allowList)
|
|
42
|
+
|
|
43
|
+
`exposeMetrics:true` 일 때만 라우트가 생긴다(디폴트는 Fastify 404). `/health` 와 같은 보안 면제(`HEALTH_EXEMPT`)이지만, 그 위에 **IP allowList** 를 한 겹 더 둔다.
|
|
44
|
+
|
|
45
|
+
- allowList 항목 = **정확한 IP**(IPv4/IPv6) 또는 **IPv4 CIDR**(`10.0.0.0/8`).
|
|
46
|
+
- **빈 배열/미지정 = 전부 허용**(사이드카·내부망 전제, 운영자 결정).
|
|
47
|
+
- 불허 IP 는 `403 forbidden`(raw text). IPv4-mapped IPv6(`::ffff:1.2.3.4`)는 IPv4 로 정규화해 비교.
|
|
48
|
+
- 잘못된 CIDR 항목은 매치 실패로 간주(fail-closed).
|
|
49
|
+
|
|
50
|
+
응답은 문자열(Prometheus 텍스트)이라 Fastify 가 envelope JSON 으로 안 감싼다(raw 전송). Content-Type 은 `MegaMetrics.PROM_CONTENT_TYPE`(`text/plain; version=0.0.4`).
|
|
51
|
+
|
|
52
|
+
### 1.4 인스트루먼트 (무엇을 재나)
|
|
53
|
+
|
|
54
|
+
이름은 `mega_` prefix + snake_case + base-unit suffix(Prometheus 컨벤션). 히스토그램 latency 버킷은 **초 단위**(5ms~10s).
|
|
55
|
+
|
|
56
|
+
| 메트릭 | 타입 | 주요 라벨 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `mega_http_requests_total` | counter | method, route, status_code, app |
|
|
59
|
+
| `mega_http_request_duration_seconds` | histogram | method, route, app |
|
|
60
|
+
| `mega_ws_messages_total` / `mega_ws_message_duration_seconds` | counter / histogram | type, ns, app |
|
|
61
|
+
| `mega_adapter_calls_total` / `mega_adapter_call_duration_seconds` | counter / histogram | domain, driver, call, status |
|
|
62
|
+
| `mega_jobs_total` | counter | queue, event(enqueued\|processed\|retried\|dlq) |
|
|
63
|
+
| `mega_sessions_total` | counter | driver, event(created\|destroyed) |
|
|
64
|
+
| `mega_bruteforce_events_total` | counter | namespace, event(check\|fail\|lockout\|reset) |
|
|
65
|
+
| `mega_upload_files_total` / `mega_upload_file_bytes` | counter / histogram | app, result |
|
|
66
|
+
| `mega_i18n_events_total` | counter | app, lang, scope, event(request\|missing) |
|
|
67
|
+
| `mega_template_renders_total` / `mega_template_render_duration_seconds` | counter / histogram | app, result |
|
|
68
|
+
| `mega_process_memory_bytes` / `mega_process_uptime_seconds` / `mega_process_cpu_seconds_total` | gauge / gauge / counter | kind, type |
|
|
69
|
+
|
|
70
|
+
`process_*` 게이지는 scrape 시점 콜백으로 현재값을 관측(observable)한다.
|
|
71
|
+
|
|
72
|
+
### 1.5 잡 메트릭 자동 수집 (subscribeJobs, ADR-132)
|
|
73
|
+
|
|
74
|
+
잡 워커/큐의 이벤트를 구독해 `mega_jobs_total` 로 변환한다. 큐가 방출하는 6종(`dispatch/start/done/retry/fail/dlq`) 중 **카운터로 의미 있는 4종만** 매핑한다:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
dispatch → enqueued done → processed retry → retried dlq → dlq
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`MegaJobWorker` 는 하부 `MegaJobQueue` 의 이벤트를 그대로 재방출하므로 **워커 1곳만 구독하면** 그 워커가 든 모든 큐의 잡이 집계된다. 같은 emitter 를 다시 구독하면 기존 해제 함수를 반환해 중복 부착을 막는다.
|
|
81
|
+
|
|
82
|
+
### 1.6 WS 메트릭
|
|
83
|
+
|
|
84
|
+
WS 수신 배선(`src/core/ws-upgrade.js`)이 메시지마다 `MegaMetrics.recordWs({ type, ns, durationMs, app })` 를 호출한다. `type` 은 라우트 키처럼 bounded 한 값만 — raw payload 는 라벨에 절대 안 넣는다.
|
|
85
|
+
|
|
86
|
+
### 1.7 클러스터 집계 (cluster-metrics, ADR-170)
|
|
87
|
+
|
|
88
|
+
`mega start` 클러스터 모드는 같은 포트를 여러 워커가 나눠 받는다. 메트릭은 **프로세스마다 따로** 쌓이므로, `/metrics` 를 한 번 긁으면 그때 응답한 워커 숫자만 나온다(새로고침마다 들쭉날쭉). OTel SDK 엔 프로세스 간 합산이 없어 우리가 한다(prom-client `AggregatorRegistry` 패턴).
|
|
89
|
+
|
|
90
|
+
흐름(마스터가 IPC relay — 워커끼리 직통 불가):
|
|
91
|
+
|
|
92
|
+
1. `/metrics` 가 한 워커에 도착 → 그 워커가 마스터에 `request`(`collectCluster`).
|
|
93
|
+
2. 마스터가 전 워커에 `collect` fan-out → 각 워커가 자기 `collect()` 텍스트를 `collected` 로 회신.
|
|
94
|
+
3. 마스터가 전원 응답(또는 timeout)까지 모아 `mergeExposition` 으로 합산 → 요청 워커에 `aggregated` 회신.
|
|
95
|
+
4. 요청 워커가 합산 텍스트로 응답.
|
|
96
|
+
|
|
97
|
+
`mergeExposition` 규칙: counter/histogram 은 **합산**, gauge 도 합산(메모리·CPU 클러스터 합이 유의미), `_info`/`target_info` 메타는 첫 값 유지(N배 방지). 단일 프로세스면 `collectCluster` 가 그냥 로컬 `collect()` 를 돌려준다. timeout/마스터 부재 시 로컬 폴백(스크레이프가 안 멈추도록).
|
|
98
|
+
|
|
99
|
+
라우트는 항상 `collectCluster()` 를 부르므로 단일/클러스터 양쪽에서 동일하게 동작한다(`src/core/mega-app.js`).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 2. Swagger / OpenAPI
|
|
104
|
+
|
|
105
|
+
> 소스: `src/core/openapi.js`, 라우트 메타 `src/core/router.js`. (ADR-070 / ADR-140)
|
|
106
|
+
|
|
107
|
+
### 2.1 무엇인가
|
|
108
|
+
|
|
109
|
+
"이 API 가 어떤 주소로 뭘 받고 뭘 주는지" 설명서를 자동으로 만들어 `/docs` 웹페이지로 보여준다. 라우트에 적어둔 JSON Schema 와 `openapi` 메타를 `@fastify/swagger` 가 `onRoute` 로 긁어 OpenAPI 3.x 명세로 만든다 — 손으로 문서 쓰다 어긋날 일이 없다.
|
|
110
|
+
|
|
111
|
+
**디폴트 OFF** 다. 옵트인이 아니면 명세·UI 라우트 자체가 없어 프로덕션 API surface 가 외부에 안 드러난다.
|
|
112
|
+
|
|
113
|
+
### 2.2 켜기 (app.config.js)
|
|
114
|
+
|
|
115
|
+
앱별 `app.config.js` 의 `openapi` 를 `enabled:true` 로 켠다.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// apps/main/app.config.js
|
|
119
|
+
import { webRequireAuth } from './middleware/web-auth.js'
|
|
120
|
+
|
|
121
|
+
export default {
|
|
122
|
+
openapi: {
|
|
123
|
+
enabled: true, // 디폴트 OFF — 명시적으로 켜야 함
|
|
124
|
+
path: '/docs', // swagger-ui 마운트 경로 (디폴트 '/docs')
|
|
125
|
+
info: {
|
|
126
|
+
title: 'sample-crud API',
|
|
127
|
+
version: '0.1.0',
|
|
128
|
+
description: 'JSON REST API 명세 (users/notes/redis).',
|
|
129
|
+
},
|
|
130
|
+
auth: [webRequireAuth], // docs 접근 가드 (빈 배열이면 공개)
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> `enabled: process.env.NODE_ENV !== 'production'` 패턴으로 dev 만 노출할 수 있다. `@fastify/swagger` 는 라우트 등록보다 **먼저** 등록돼야 onRoute 로 스키마를 수집하므로 코어가 보안·정적 자산 뒤·`/health` 앞에서 등록한다. 멀티앱은 각자 Fastify 인스턴스라 docs 경로 충돌이 없다.
|
|
136
|
+
|
|
137
|
+
### 2.3 라우트 메타
|
|
138
|
+
|
|
139
|
+
라우트 옵션의 `openapi:{ tags, summary, description, deprecated }` 가 라우트 schema 의 비검증 메타 필드로 병합돼 명세에 반영된다. 검증용 `schema`(body/params/response)와 함께 쓴다.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
// apps/main/routes/users.js
|
|
143
|
+
export default (router) => {
|
|
144
|
+
const guarded = { before: [requireAuth] }
|
|
145
|
+
|
|
146
|
+
router.http.get('/users', UserController.index, {
|
|
147
|
+
...guarded,
|
|
148
|
+
openapi: { tags: ['users'], summary: '사용자 목록', description: '모든 사용자를 반환한다.' },
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
router.http.post('/users', UserController.create, {
|
|
152
|
+
...guarded,
|
|
153
|
+
schema: { body: userBody }, // 검증 + 명세 둘 다에 반영
|
|
154
|
+
openapi: { tags: ['users'], summary: '사용자 생성', description: 'name·email 로 사용자를 생성한다.' },
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 2.4 인증 가드
|
|
160
|
+
|
|
161
|
+
`auth` 배열은 swagger-ui 의 `preHandler` 로 걸린다. 핸들러와 **동일한 `(req, reply, ctx)` 시그니처**(빈 배열=공개). 샘플은 `webRequireAuth` 로 보호해 비로그인 브라우저를 로그인 페이지로 보낸다 — 데모 API 문서도 로그인이 필요하다.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 3. Tracing (OpenTelemetry)
|
|
166
|
+
|
|
167
|
+
> 소스: `src/lib/mega-tracing.js`, WS 통합 `src/core/ws-upgrade.js`, boot 배선 `src/core/boot.js`. (ADR-104 / ADR-114 / ADR-126)
|
|
168
|
+
|
|
169
|
+
### 3.1 무엇인가
|
|
170
|
+
|
|
171
|
+
요청 하나가 어떤 어댑터 호출(DB query, cache get …)을 거쳐갔는지 **span 트리**로 추적한다. 어댑터의 관찰성 hook(`onCallStart`/`onCallEnd`) 위에 자동 span 을 얹는다 — 코어·어댑터 코드는 한 줄도 안 바꾼다. 옵트인 OFF 면 어댑터에 리스너가 안 붙어 **0 비용**.
|
|
172
|
+
|
|
173
|
+
### 3.2 켜기 (env)
|
|
174
|
+
|
|
175
|
+
트레이싱은 **환경변수**로 켠다. boot 가 `MegaTracing.fromEnv()` 를 호출해 `MEGA_OTEL_ENABLED=true` 면 초기화 + 어댑터 일괄 구독한다.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
MEGA_OTEL_ENABLED=true # true 일 때만 활성 (serviceName 필수)
|
|
179
|
+
MEGA_OTEL_SERVICE_NAME=mega-app # service.name (활성 시 필수)
|
|
180
|
+
MEGA_OTEL_ENDPOINT=http://localhost:4318/v1/traces # exporter endpoint
|
|
181
|
+
MEGA_OTEL_EXPORTER=otlp # otlp | zipkin | console | inmemory
|
|
182
|
+
MEGA_OTEL_SAMPLING_RATIO=1.0 # 0~1 비율, 또는 always_on / always_off
|
|
183
|
+
MEGA_OTEL_VERSION= # service.version (옵션)
|
|
184
|
+
MEGA_OTEL_ENVIRONMENT=production # deployment.environment.name (옵션)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> ⚠️ prefix 없는 `OTEL_ENABLED`/`OTEL_ENDPOINT` 는 죽은 키다 — 반드시 `MEGA_OTEL_*`. Zipkin 으로 보내려면 `MEGA_OTEL_EXPORTER=zipkin` + `MEGA_OTEL_ENDPOINT=http://localhost:9411/api/v2/spans`.
|
|
188
|
+
|
|
189
|
+
### 3.3 exporter / sampler / processor
|
|
190
|
+
|
|
191
|
+
| 옵션 | 값 | 폴백 |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| exporter | `console` / `otlp` / `zipkin` / `inmemory` / SpanExporter 인스턴스 | endpoint 있으면 `otlp`, 없으면 `console` |
|
|
194
|
+
| sampler | `always_on` / `always_off` / `traceidratio:0.1` / 숫자 `0.1` | `always_on` |
|
|
195
|
+
| processor | `simple` / `batch` | otlp·zipkin=`batch`, 그 외=`simple` |
|
|
196
|
+
|
|
197
|
+
비율 샘플러는 `ParentBasedSampler` 로 감싸 자식 span 이 부모 결정을 따른다. 잘못된 값은 `MegaConfigError`(`tracing.invalid_sampling` / `tracing.invalid_exporter`)로 fail-fast.
|
|
198
|
+
|
|
199
|
+
### 3.4 context 전파: AsyncLocalStorage (ALS)
|
|
200
|
+
|
|
201
|
+
두 개의 ALS 가 협력한다.
|
|
202
|
+
|
|
203
|
+
- **요청 단위 active context**(`mega-tracing.js` 내부) — HTTP/WS 루트 span 과 `ctx.tracer.span` 이 "지금 열린 span" 을 비동기 흐름 전반에 전파.
|
|
204
|
+
- **per-adapter `#callScope`**(`MegaAdapter`) — 어댑터 자동 span(ADR-114)의 부모-자식 추적.
|
|
205
|
+
|
|
206
|
+
어댑터 스코프가 없을 때(핸들러 최상위의 어댑터 호출)는 요청 ALS 를 부모로 폴백해, 어댑터 span 이 HTTP/WS 루트 span 의 자식으로 정확히 중첩된다. `context-async-hooks` 패키지를 도입하지 않고 우리 ALS 로 같은 효과를 낸다.
|
|
207
|
+
|
|
208
|
+
### 3.5 run() seam (옵트인 OFF 면 NOOP_SPAN)
|
|
209
|
+
|
|
210
|
+
`onCallStart` 이 **스코프 토큰**(OTel span context)을 반환하면, 베이스 `_instrument` 가 도메인 `fn` 을 `#callScope.run(token, fn)` 안에서 실행한다. 그래서 `fn` 내부의 중첩 호출은 부모 토큰을 읽어 자기 span 의 부모로 삼는다. `run()` 은 각 호출을 독립 스코프로 격리하므로 **순차·중첩·동시(`Promise.all`) 전부 정확**하다 — 과거 `enterWith` 가 공유 동기 실행을 오염시켜 sibling 을 잘못 묶던 문제를 구조적으로 제거한다.
|
|
211
|
+
|
|
212
|
+
옵트인 OFF 면 `ctx.tracer.span(name, fn)` 이 `fn(NOOP_SPAN)` 만 호출한다 — span 없이 함수만 실행, **0 비용**. NOOP_SPAN 은 메서드가 다 있지만 아무것도 기록 안 한다.
|
|
213
|
+
|
|
214
|
+
### 3.6 사용자 직접 span — `ctx.tracer.span`
|
|
215
|
+
|
|
216
|
+
핸들러 안에서 임의 작업을 span 으로 감쌀 수 있다. 활성 HTTP/WS span 의 자식으로 열리고, 안의 어댑터 호출도 자동으로 자식 span 으로 중첩된다.
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
// 데모: ctx.tracer.span 으로 'demo.tracing.work' 자식 span + DB 핑
|
|
220
|
+
await ctx.tracer.span('demo.tracing.work', async (span) => {
|
|
221
|
+
span.setAttribute('demo.kind', 'manual-generate')
|
|
222
|
+
await ctx.db('db').query('SELECT 1') // 어댑터 자동 span 도 자식으로 중첩
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`fn` 이 throw/reject 하면 span 에 ERROR + recordException 후 **그대로 재전파**(silent 금지). `ctx.tracer.traceId` / `ctx.tracer.spanId` / `ctx.tracer.activeSpan()` 로 현재 ID 도 읽는다.
|
|
227
|
+
|
|
228
|
+
### 3.7 HTTP/WS 루트 span
|
|
229
|
+
|
|
230
|
+
`enterHttpSpan` 이 `onRequest` 에서 열고(`enterWith` 로 활성화), `onError` 에서 `setError`, `onResponse` 에서 `finish(statusCode)` 로 닫는다. OTel 시맨틱(SERVER span 은 **5xx 만 ERROR**, 4xx 는 클라이언트 잘못이라 UNSET 유지)을 한곳에 모아 배선 코드가 OTel enum 을 안 만지게 한다.
|
|
231
|
+
|
|
232
|
+
### 3.8 shutdown
|
|
233
|
+
|
|
234
|
+
`shutdown()` 은 리스너 해제 + 남은 span flush + provider 종료 + ALS 비활성화를 멱등하게 한다. boot 가 `MegaShutdown` 에 등록한다. `state`(활성) 여부와 무관하게 토큰의 span 을 닫아 in-flight span 누수를 막는다(shutdown 직후 케이스).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 4. Logger (pino)
|
|
239
|
+
|
|
240
|
+
> 소스: `src/lib/mega-logger.js`, `src/lib/logger/telegram-transport.js`, `src/lib/logger/telegram-core.js`. (ADR-023 / ADR-141 / ADR-166)
|
|
241
|
+
|
|
242
|
+
### 4.1 무엇인가
|
|
243
|
+
|
|
244
|
+
검증된 `pino`(아주 빠른 JSON 로거) 위에, `mega.config.js` 의 `logger` 설정대로 **여러 출력처(sink)** 를 동시에 연결한다. 무거운 IO(파일·텔레그램)는 pino transport 가 **worker thread 에서** 돌려 이벤트루프를 보호한다.
|
|
245
|
+
|
|
246
|
+
### 4.2 켜기 (config — env 아님)
|
|
247
|
+
|
|
248
|
+
로거는 `mega.config.js` 의 `logger` 객체로 설정한다. **`MEGA_LOG_*` 같은 env 키는 코드가 읽지 않는다** — `LOG_LEVEL` env 도 안 읽는다(레벨은 `logger.level` 로). boot 가 한 번 만들어(`buildLogger`) 모든 앱이 공유한다(worker thread·파일 핸들 1벌).
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
// mega.config.js
|
|
252
|
+
export default {
|
|
253
|
+
logger: {
|
|
254
|
+
level: 'debug', // 전역 레벨
|
|
255
|
+
sinks: [{ type: 'console', pretty: true }], // 여러 sink 동시 가능
|
|
256
|
+
// 시크릿 마스킹 — sink 출력 전 메인스레드에서 적용(transport 엔 이미 시크릿 없음)
|
|
257
|
+
redact: ['*.password', '*.token', '*.secret', '*.authorization'],
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`sinks` 가 비었거나 `logger` 미설정이면 `null` → 앱은 `logger:false`(무로그).
|
|
263
|
+
|
|
264
|
+
### 4.3 sink 종류
|
|
265
|
+
|
|
266
|
+
| type | 동작 | 옵션 |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| `console` | `pretty:true` → pino-pretty(dev), 아니면 stdout JSON 한 줄(prod) | `pretty` |
|
|
269
|
+
| `file` | pino-roll 날짜별 로테이션 + keep N개 | `path`, `rotation`(디폴트 `daily`), `keep` |
|
|
270
|
+
| `telegram` | warn 이상만, worker thread 비동기 전송 + 실패 시 disk retry | `botToken`, `chatId`, `throttleMax`, `throttleWindowMs`, `retryDir` |
|
|
271
|
+
|
|
272
|
+
각 sink 는 자기 `level` 을 줄 수 있다(미지정 시 전역 `level`). 텔레그램은 디폴트가 `warn`.
|
|
273
|
+
|
|
274
|
+
> 시크릿(botToken 등)은 `.env` 에서 읽어 config 로 주입한다 — 코드·config 파일에 직접 적지 않는다.
|
|
275
|
+
|
|
276
|
+
### 4.4 trace_id 자동 주입 (ALS 통합)
|
|
277
|
+
|
|
278
|
+
pino `mixin` 에 `MegaTracing.logMixin` 이 배선돼, 활성 span 이 있으면 **모든 로그 라인에 `{ trace_id, span_id }`** 가 자동 첨부된다. 로그와 trace 가 상관(correlate)된다. 활성 span 없거나 트레이싱 OFF 면 빈 객체(0 비용). Fastify 는 `req.log` 에 `reqId` 도 자동 바인딩한다(요청별 상관).
|
|
279
|
+
|
|
280
|
+
### 4.5 텔레그램 transport (alarm)
|
|
281
|
+
|
|
282
|
+
`warn` 이상 로그를 텔레그램으로 보내는 알람 sink. 순수 로직은 `telegram-core.js`(단위 테스트), worker glue 는 `telegram-transport.js`.
|
|
283
|
+
|
|
284
|
+
- **throttle**: 슬라이딩 윈도우 — 최근 `windowMs` 안 `max` 건까지(폭주 시 텔레그램 rate limit 직격 방지). 초과분 드롭.
|
|
285
|
+
- **RetryQueue (disk-backed)**: 전송 실패분을 JSONL 파일에 쌓고 주기 드레인(기본 30초)으로 재전송 — 전송 실패해도 메시지를 잃지 않는다.
|
|
286
|
+
- **httpsPost timeout/error 핸들러** (ADR-166): 응답이 안 오면 `timeoutMs`(기본 10초) 후 끊어 retry queue 로 넘긴다. 응답 스트림 `error` 도 reject 로 흡수해 워커가 안 죽게 한다.
|
|
287
|
+
|
|
288
|
+
#### drain race 정정 (ADR-166)
|
|
289
|
+
|
|
290
|
+
RetryQueue 가 큐를 비울 때 `read → writeFile('')` 를 쓰면 두 await 사이에 끼어든 `append`(write 경로의 재시도)를 통째로 지운다. 그래서 `rename` 으로 **단일 syscall** 가로채기를 쓴다 — 가로챈 뒤 들어오는 append 는 새 파일에 쌓여 유실되지 않는다. 동시 drain 이 한 번 더 들어오면 파일이 이미 옮겨져 `ENOENT` → 빈 배열(중복 처리 없음). **RetryQueue 에 의존하는 코드는 이 race 정정을 깨지 않도록 주의.**
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 5. sample/crud 데모 (ADR-163)
|
|
295
|
+
|
|
296
|
+
> 소스: `sample/crud/apps/main/routes/{metrics,tracing,logs}.js`, 동명 controllers. 모두 `webRequireAuth` 로 보호(비로그인은 로그인 페이지로), 자동 라우트 로딩(`loadRoutes`).
|
|
297
|
+
|
|
298
|
+
| 데모 | 라우트 | 보여주는 것 |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `/demo/metrics` | GET | `MegaMetrics.collect()` 를 파싱한 사람 친화 카드(HTTP/잡/WS/process) + raw `/metrics` 링크 |
|
|
301
|
+
| `/demo/tracing` | GET, POST `/generate` | 현재 trace_id(응답 헤더 `x-trace-id` 에도) + Zipkin 딥링크. "생성" 버튼이 `ctx.tracer.span` 으로 자식 span + DB 핑 → Zipkin 에 span 트리 |
|
|
302
|
+
| `/demo/logs` | GET, POST `/emit` | 선택 레벨로 실제 pino 로그 emit → 마스킹·trace_id 첨부·구조적 NDJSON 은 서버 콘솔에서 확인 |
|
|
303
|
+
|
|
304
|
+
데모 데이터는 `ctx.services.{metricsDemo,tracingDemo,logsDemo}`(자동 DI)가 만든다. 폼은 POST(PRG) + CSRF 토큰.
|
|
305
|
+
|
|
306
|
+
> `/demo/upload`(multipart)는 별도 가이드(업로드 편)에서 다룬다.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 6. 환경변수 (관측 관련, 정본)
|
|
311
|
+
|
|
312
|
+
> ⚠️ 여기 적힌 키가 **소스가 실제로 읽는 키**다(`src/lib/mega-tracing.js`·`mega-metrics.js` `fromEnv`, `.env.example`). 짧은 별칭(`OTEL_ENDPOINT`, `ZIPKIN_URL`, `MEGA_OTEL_SAMPLING_RATE`, `MEGA_LOG_*`, `MEGA_METRICS_ALLOWED_IPS`)은 **존재하지 않는다** — 혼동 주의.
|
|
313
|
+
|
|
314
|
+
### 트레이싱 (MegaTracing.fromEnv 가 읽음)
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
MEGA_OTEL_ENABLED=false # true 면 활성 (serviceName 필수)
|
|
318
|
+
MEGA_OTEL_SERVICE_NAME=mega-app # service.name
|
|
319
|
+
MEGA_OTEL_ENDPOINT=... # exporter endpoint (otlp: /v1/traces, zipkin: /api/v2/spans)
|
|
320
|
+
MEGA_OTEL_EXPORTER=otlp # otlp | zipkin | console | inmemory
|
|
321
|
+
MEGA_OTEL_SAMPLING_RATIO=1.0 # 0~1 또는 always_on / always_off ← RATE 아님 RATIO
|
|
322
|
+
MEGA_OTEL_VERSION= # 옵션
|
|
323
|
+
MEGA_OTEL_ENVIRONMENT= # 옵션
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 메트릭
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# 표준 boot 경로: config(mega.config.js)의 health.exposeMetrics 로 켠다.
|
|
330
|
+
# 샘플 관례: exposeMetrics: process.env.METRICS_ENABLED === 'true'
|
|
331
|
+
METRICS_ENABLED=true # 위 관례를 쓰는 config 에서만 의미 있음 (아래 6.1 갭 참조)
|
|
332
|
+
|
|
333
|
+
# MEGA_METRICS_* 는 MegaMetrics.fromEnv()(public API) 전용 키 — 표준 boot 는 호출 안 함:
|
|
334
|
+
# MEGA_METRICS_ENABLED=true
|
|
335
|
+
# MEGA_METRICS_SERVICE_NAME=mega # 없으면 MEGA_OTEL_SERVICE_NAME 폴백
|
|
336
|
+
# MEGA_METRICS_VERSION=
|
|
337
|
+
# MEGA_METRICS_ENVIRONMENT=
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**메트릭 IP allowList 는 env 가 아니다** — `mega.config.js` 의 `health.metricsAllowList: ['127.0.0.1', '::1']` 로 설정한다.
|
|
341
|
+
|
|
342
|
+
### 로거
|
|
343
|
+
|
|
344
|
+
env 키 없음 — 전부 `mega.config.js` 의 `logger:{ level, sinks, redact }` 로 설정한다(텔레그램 botToken/chatId 만 `.env` 에서 읽어 config 로 주입).
|
|
345
|
+
|
|
346
|
+
### collector 인프라 (앱이 아니라 docker collector 가 읽음)
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
MEGA_OTEL_OTLP_ENDPOINT=http://localhost:4318/v1/traces # 테스트/인프라용 (앱 exporter 는 MEGA_OTEL_ENDPOINT)
|
|
350
|
+
MEGA_OTEL_ZIPKIN_API=http://localhost:9411/api/v2 # 테스트가 span 도달 폴링
|
|
351
|
+
MEGA_OTEL_HEALTH_URL=http://localhost:13133 # collector health_check
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 6.1 METRICS_ENABLED 문서/코드 갭 (OQ 칩 참조)
|
|
355
|
+
|
|
356
|
+
`MegaMetrics.fromEnv()` 는 `METRICS_ENABLED`/`MEGA_METRICS_ENABLED` 를 읽도록 구현돼 있지만, **표준 boot 경로(`src/core/boot.js`)는 `MegaMetrics.fromEnv()` 를 호출하지 않는다**(트레이싱과 달리 메트릭은 config `health.exposeMetrics` 로만 배선). 따라서 `METRICS_ENABLED=true` 만 설정해도 boot 가 직접 켜지는 않는다 — `mega.config.js` 에서 `exposeMetrics: process.env.METRICS_ENABLED === 'true'` 처럼 **config 가 그 env 를 읽어 와이어**해야 동작한다. `MegaMetrics.fromEnv()` 는 라이브러리를 직접 임베드하는 경우의 public API 다. (잔존 OQ 칩 — `docs/09` 참조)
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 7. 함정
|
|
361
|
+
|
|
362
|
+
- **`/metrics` 카디널리티 폭발**: HTTP route 라벨은 **매칭된 라우트 패턴**(`/users/:id`)만 쓴다. 매칭 안 된 404 의 raw path 를 라벨에 넣으면 무한 카디널리티(공격자가 임의 경로 폭격 → 메모리 폭증)가 되므로 고정 라벨 `__unmatched__` 로 접는다. 파일명·MIME·이메일·view 경로 같은 unbounded 값은 라벨에 절대 안 넣는다(PII·카디널리티).
|
|
363
|
+
- **트레이싱 옵트인 OFF**: `ctx.tracer.span` 은 `NOOP_SPAN` 으로 깨지지 않고 함수만 실행한다(성능 영향 0). 코드에서 트레이싱 분기 처리할 필요 없다.
|
|
364
|
+
- **`enterWith` 격리 조건**: 시작/종료 분리형 span(`enterSpan`/`enterHttpSpan`)의 `enterWith` 는 **요청별로 격리된 비동기 흐름**(HTTP 요청·WS 메시지 콜백)에서만 호출해야 한다. 공유 동기 스코프에서 부르면 sibling 오염 위험 — 어댑터 자동 span 은 그래서 `run()` seam 을 쓴다.
|
|
365
|
+
- **logger drain race**: RetryQueue 는 `rename` 단일 syscall 로 큐를 가로챈다(ADR-166). `read → writeFile('')` 로 바꾸면 동시 append 가 유실된다 — 의존 코드 수정 시 주의.
|
|
366
|
+
- **telegram httpsPost**: timeout(기본 10초)·response `error` 핸들러가 없으면 무응답/스트림 끊김에 워커가 영구 정지하거나 죽는다(ADR-166). 주입 시그니처를 바꿀 때 이 두 핸들러를 유지할 것.
|
|
367
|
+
- **클러스터 gauge 합산 한계**: `mergeExposition` 은 gauge 를 합산한다 — 프로세스별 gauge(uptime 등)는 클러스터에서 N배로 보이는 문서화된 한계(prom-client 기본도 gauge=sum). `_info`/`target_info` 만 첫 값 유지.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 관련 ADR
|
|
372
|
+
|
|
373
|
+
ADR-104(트레이싱), ADR-114·ADR-126(run/ALS), ADR-131(메트릭), ADR-132(subscribeJobs), ADR-140(swagger), ADR-141(pino), ADR-163(관측 sample), ADR-166(logger drain race + httpsPost timeout), ADR-170(cluster-metrics IPC seam).
|
|
@@ -175,4 +175,11 @@ export default {
|
|
|
175
175
|
// OpenTelemetry 분산 트레이싱 — **config 블록이 아니라 .env 의 MEGA_OTEL_\*** 로 설정한다
|
|
176
176
|
// (MegaTracing.fromEnv, boot.js). MEGA_OTEL_ENABLED='true' + MEGA_OTEL_SERVICE_NAME 필수.
|
|
177
177
|
// (`tracing` 키는 스키마엔 있으나 현재 부팅이 소비하지 않음 — 죽은 설정 회피 위해 블록을 두지 않음.)
|
|
178
|
+
|
|
179
|
+
// dev watch(`mega start --watch`) ignore — .env 의 WATCH_IGNORE(콤마 구분 glob)가 정본이다
|
|
180
|
+
// (ADR-220, 숨은 코드 디폴트 없음). 폴더명을 바꾼 프로젝트는 .env 의 그 줄만 고치면 된다.
|
|
181
|
+
// 미설정이면 ignore 0 — 모든 변경이 재시작 대상(기동 시 경고 1줄).
|
|
182
|
+
watch: {
|
|
183
|
+
ignore: (process.env.WATCH_IGNORE ?? '').split(',').map((p) => p.trim()).filter(Boolean),
|
|
184
|
+
},
|
|
178
185
|
}
|
package/sample/crud/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"node": ">=20"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "mega start --watch",
|
|
10
|
+
"dev": "NODE_ENV=development mega start --watch",
|
|
11
11
|
"start": "NODE_ENV=production mega start",
|
|
12
12
|
"migrate": "mega migrate",
|
|
13
13
|
"migrate:down": "mega migrate:down",
|
|
@@ -25,4 +25,4 @@
|
|
|
25
25
|
"concurrently": "^9.0.0",
|
|
26
26
|
"vitest": "^4.1.8"
|
|
27
27
|
}
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# sample/crud — WS Hub 서버 기동 스크립트 (ADR-032/176)
|
|
4
4
|
#
|
|
5
5
|
# 별도 hub 프로세스(`mega-ws-hub` 바이너리, mega-framework bin)를 localhost:3100 에 띄운다.
|
|
6
|
-
# 앱(`
|
|
6
|
+
# 앱(`npm run dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
|
|
7
7
|
#
|
|
8
8
|
# ⚠️ `mega-ws-hub` 는 `mega start` 와 달리 `.env` 를 자동 로드하지 않는다(직접 process.env 만 읽음,
|
|
9
9
|
# src/lib/ws-hub.js). 그래서 Node 20.6+ 의 `--env-file` 로 .env 를 주입한다.
|
|
@@ -22,15 +22,29 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
22
22
|
cd "$ROOT"
|
|
23
23
|
|
|
24
24
|
ENV_FILE=".env"
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
# mega-ws-hub 바이너리 탐색 — 두 설치 형상을 모두 지원한다:
|
|
27
|
+
# ① 독립 프로젝트(mega new 스캐폴드): 자기 node_modules 에 설치됨.
|
|
28
|
+
# ② 모노레포 workspaces(ADR-197): 의존성이 레포 루트로 hoist 되어 sample/crud 에는 node_modules 가
|
|
29
|
+
# 없다(정상). 루트의 mega-framework self-link 를 따라간다.
|
|
30
|
+
BIN=""
|
|
31
|
+
for candidate in \
|
|
32
|
+
"node_modules/mega-framework/bin/mega-ws-hub.js" \
|
|
33
|
+
"../../node_modules/mega-framework/bin/mega-ws-hub.js"; do
|
|
34
|
+
if [ -f "$candidate" ]; then
|
|
35
|
+
BIN="$candidate"
|
|
36
|
+
break
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
26
39
|
|
|
27
40
|
# 사전 점검 — 누락 시 silent 진행 금지, 이유를 명확히 알린다(P4/P7).
|
|
28
41
|
[ -f "$ENV_FILE" ] || {
|
|
29
42
|
echo "✗ $ROOT/$ENV_FILE 가 없습니다 — MEGA_WSHUB_TOKENS 등 허브 설정이 필요합니다." >&2
|
|
30
43
|
exit 1
|
|
31
44
|
}
|
|
32
|
-
[ -
|
|
33
|
-
echo "✗
|
|
45
|
+
[ -n "$BIN" ] || {
|
|
46
|
+
echo "✗ mega-ws-hub.js 를 찾지 못했습니다(로컬/루트 node_modules 모두) — 'npm install' 로 mega-framework 를 설치하세요." >&2
|
|
47
|
+
echo " (모노레포에서는 레포 루트에서 실행해야 합니다. yarn 은 미지원 — npm 사용.)" >&2
|
|
34
48
|
exit 1
|
|
35
49
|
}
|
|
36
50
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
한글 PDF 다운로드 재현용 텍스트
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
한글 PDF 다운로드 재현용 텍스트
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.8","results":[[":test/apps/main/index.test.js",{"duration":1.6905000000000001,"failed":false}]]}
|
|
@@ -165,6 +165,9 @@ export function resolveConnection(config, { driver, dbKey = 'database', dbConfli
|
|
|
165
165
|
return out
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** pool acquire 대기 한도 프레임워크 디폴트(ms) — 명시 0 으로 드라이버 무한 대기 옵트인(ADR-216). */
|
|
169
|
+
export const DEFAULT_ACQUIRE_TIMEOUT_MS = 10_000
|
|
170
|
+
|
|
168
171
|
/**
|
|
169
172
|
* pg 풀 매핑 — 값이 null 이면 미지원(throw), `{ key, divideBy? }` 면 키 이름 변경(+단위 변환).
|
|
170
173
|
* @type {Record<string, { key: string, divideBy?: number } | null>}
|
|
@@ -216,11 +219,11 @@ export const MONGO_POOL_SPEC = {
|
|
|
216
219
|
* @returns {Record<string, number>} 드라이버 풀 옵션 객체(빈 객체 가능).
|
|
217
220
|
*/
|
|
218
221
|
export function normalizePool(pool, spec, driver) {
|
|
219
|
-
if (pool === undefined) return {}
|
|
220
|
-
assertPlainObject('pool', pool, { driver })
|
|
221
222
|
/** @type {Record<string, number>} */
|
|
222
223
|
const out = {}
|
|
223
|
-
|
|
224
|
+
if (pool !== undefined) assertPlainObject('pool', pool, { driver })
|
|
225
|
+
const entries = pool === undefined ? [] : Object.entries(/** @type {Record<string, unknown>} */ (pool))
|
|
226
|
+
for (const [key, value] of entries) {
|
|
224
227
|
if (value === undefined) continue
|
|
225
228
|
const map = spec[key]
|
|
226
229
|
if (map === undefined) {
|
|
@@ -234,5 +237,13 @@ export function normalizePool(pool, spec, driver) {
|
|
|
234
237
|
const num = /** @type {number} */ (value)
|
|
235
238
|
out[map.key] = map.divideBy ? Math.floor(num / map.divideBy) : num
|
|
236
239
|
}
|
|
240
|
+
// 프레임워크 디폴트(ADR-216 G2 H-1): acquire 무한 대기(pg connectionTimeoutMillis=0 ·
|
|
241
|
+
// mongo waitQueueTimeoutMS=0 드라이버 디폴트)는 연결 leak 1건을 전체 서비스 행으로 키운다 —
|
|
242
|
+
// 미지정 시 10s 를 기본 적용한다(maria 드라이버 디폴트 10s 와 정렬). 명시 `acquireTimeoutMs: 0`
|
|
243
|
+
// 은 드라이버 무한 대기 의미 그대로 통과(무한 옵트인).
|
|
244
|
+
const acquireMap = spec.acquireTimeoutMs
|
|
245
|
+
if (acquireMap !== null && acquireMap !== undefined && out[acquireMap.key] === undefined) {
|
|
246
|
+
out[acquireMap.key] = DEFAULT_ACQUIRE_TIMEOUT_MS
|
|
247
|
+
}
|
|
237
248
|
return out
|
|
238
249
|
}
|
|
@@ -75,6 +75,8 @@ const KNOWN_OPTIONS = new Set(['serializer', 'extension'])
|
|
|
75
75
|
* @property {string} [basePath] - 캐시 파일 저장 디렉토리 (필수).
|
|
76
76
|
* @property {string} [dir] - `basePath` 의 별칭 (ADR-082 정합, 하위 호환).
|
|
77
77
|
* @property {{ serializer?: 'json' | 'raw', extension?: string }} [options]
|
|
78
|
+
* @property {string} [namespace] - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213, 베이스 처리).
|
|
79
|
+
* @property {number} [defaultTtlSec] - `set` ttl 미지정 시 디폴트(초). 0 = 무한 옵트인. (ADR-216, 베이스 처리)
|
|
78
80
|
*/
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -94,7 +96,8 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
94
96
|
#extension
|
|
95
97
|
|
|
96
98
|
/**
|
|
97
|
-
* @param {FileConfig} [config] - services.caches.<key> 설정.
|
|
99
|
+
* @param {FileConfig} [config] - services.caches.<key> 설정. 베이스(MegaCacheAdapter)의
|
|
100
|
+
* `namespace`(ADR-064 자동 prefix)/`defaultTtlSec`(ADR-216) 도 여기서 받는다.
|
|
98
101
|
* @throws {MegaValidationError} `adapter.basepath_required` - basePath/dir 누락.
|
|
99
102
|
* @throws {MegaValidationError} `adapter.invalid_option` - 옵션 타입/미지원 키 오류.
|
|
100
103
|
*/
|
|
@@ -245,7 +248,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
245
248
|
*/
|
|
246
249
|
async get(key) {
|
|
247
250
|
return this._instrument('get', { key }, async () => {
|
|
248
|
-
const path = this.#pathFor(key)
|
|
251
|
+
const path = this.#pathFor(this._cacheKey(key))
|
|
249
252
|
let raw
|
|
250
253
|
try {
|
|
251
254
|
raw = await readFile(path, 'utf8')
|
|
@@ -278,6 +281,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
278
281
|
// TTL·직렬화 검증은 의도적으로 `_instrument` **밖**(fail-fast). 잘못된 인자는 디스크 I/O·hook·stats
|
|
279
282
|
// 이전에 거부 — 프로그래밍 오류를 instrumented 호출 통계에 섞지 않는다(L-1, redis 어댑터와 동일 결정).
|
|
280
283
|
this._assertTtl(ttl)
|
|
284
|
+
ttl = this._resolveTtl(ttl, key) // 미지정 → defaultTtlSec(ADR-216). 디폴트 값은 생성자에서 검증됨.
|
|
281
285
|
if (this.#serializer === 'raw' && typeof value !== 'string') {
|
|
282
286
|
throw new MegaValidationError('cache.unserializable', `file set("${key}"): serializer='raw' requires a string value (got ${typeof value}).`, {
|
|
283
287
|
details: { key, type: typeof value, serializer: 'raw' },
|
|
@@ -300,7 +304,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
300
304
|
value,
|
|
301
305
|
expiresAt: ttl !== undefined ? Date.now() + ttl * 1000 : null,
|
|
302
306
|
}
|
|
303
|
-
await this.#atomicWrite(this.#pathFor(key), envelope)
|
|
307
|
+
await this.#atomicWrite(this.#pathFor(this._cacheKey(key)), envelope)
|
|
304
308
|
})
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -312,7 +316,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
312
316
|
async del(key) {
|
|
313
317
|
return this._instrument('del', { key }, async () => {
|
|
314
318
|
try {
|
|
315
|
-
await unlink(this.#pathFor(key))
|
|
319
|
+
await unlink(this.#pathFor(this._cacheKey(key)))
|
|
316
320
|
} catch (err) {
|
|
317
321
|
// ENOENT = 이미 없음(del idempotent — 정상). 그 외 I/O 에러는 전파(무차별 삼킴 X).
|
|
318
322
|
if (/** @type {any} */ (err)?.code !== 'ENOENT') throw err
|
|
@@ -331,7 +335,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
331
335
|
*/
|
|
332
336
|
async has(key) {
|
|
333
337
|
return this._instrument('has', { key }, async () => {
|
|
334
|
-
const path = this.#pathFor(key)
|
|
338
|
+
const path = this.#pathFor(this._cacheKey(key))
|
|
335
339
|
let raw
|
|
336
340
|
try {
|
|
337
341
|
raw = await readFile(path, 'utf8')
|
|
@@ -199,11 +199,12 @@ export class MegaFileSessionAdapter extends MegaSessionAdapter {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* 누적 통계 + file 세션 특화.
|
|
203
|
-
*
|
|
202
|
+
* 누적 통계 + file 세션 특화. `cleanupIntervalMs` 노출(0=내부 타이머 off) — 만료 스캔 주기의
|
|
203
|
+
* 적용 여부를 운영/테스트가 확인하는 단일 창구(ADR-215).
|
|
204
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, basePath: string, ttlMs: number, cleanupIntervalMs: number }}
|
|
204
205
|
*/
|
|
205
206
|
getStats() {
|
|
206
|
-
return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs }
|
|
207
|
+
return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs, cleanupIntervalMs: this.#cleanupIntervalMs }
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
/**
|
|
@@ -298,19 +298,22 @@ export class MegaMariaAdapter extends MegaDbAdapter {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
* 누적 통계 + 풀 통계
|
|
302
|
-
*
|
|
301
|
+
* 누적 통계 + 풀 통계 — 공통 코어 키 `{ total, active, idle, waiting }` 은 4 driver 동일
|
|
302
|
+
* 형태(ADR-216 G2 H-2). `queue` 는 기존 소비자 하위 호환 별칭(= waiting). 연결 전이면 0.
|
|
303
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, active: number, idle: number, waiting: number, queue: number } }}
|
|
303
304
|
*/
|
|
304
305
|
getStats() {
|
|
305
306
|
const pool = this.#pool
|
|
307
|
+
const waiting = pool?.taskQueueSize() ?? 0
|
|
306
308
|
return {
|
|
307
309
|
...super.getStats(),
|
|
308
310
|
driver: 'mariadb',
|
|
309
311
|
pool: {
|
|
310
312
|
total: pool?.totalConnections() ?? 0,
|
|
311
|
-
idle: pool?.idleConnections() ?? 0,
|
|
312
313
|
active: pool?.activeConnections() ?? 0,
|
|
313
|
-
|
|
314
|
+
idle: pool?.idleConnections() ?? 0,
|
|
315
|
+
waiting,
|
|
316
|
+
queue: waiting,
|
|
314
317
|
},
|
|
315
318
|
}
|
|
316
319
|
}
|