mega-framework 0.1.6 → 0.1.8

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.
Files changed (248) hide show
  1. package/README.md +9 -0
  2. package/bin/mega-ws-hub.js +2 -2
  3. package/package.json +33 -9
  4. package/sample/crud/.env +10 -1
  5. package/sample/crud/.env.example +10 -1
  6. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  7. package/sample/crud/.mega/journal/snapshot.json +261 -0
  8. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  9. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  10. package/sample/crud/apps/main/locales/server/en.json +12 -1
  11. package/sample/crud/apps/main/locales/server/ko.json +12 -1
  12. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  13. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  14. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  15. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  16. package/sample/crud/apps/main/models/note-model.js +79 -0
  17. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  18. package/sample/crud/apps/main/models/user-model.js +146 -0
  19. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  20. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  21. package/sample/crud/apps/main/routes/users.js +55 -10
  22. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  23. package/sample/crud/apps/main/services/auth-service.js +39 -24
  24. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  25. package/sample/crud/apps/main/services/note-service.js +6 -6
  26. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  27. package/sample/crud/apps/main/services/user-service.js +62 -21
  28. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  29. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  30. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  31. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  32. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  33. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  34. package/sample/crud/mega.config.js +10 -2
  35. package/sample/crud/package.json +3 -3
  36. package/sample/crud/scripts/start-ws-hub.sh +20 -6
  37. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  38. package/sample/simple/package.json +2 -2
  39. package/src/adapters/adapter-manager.js +2 -1
  40. package/src/adapters/adapter-options.js +44 -3
  41. package/src/adapters/file-adapter.js +9 -5
  42. package/src/adapters/file-session-adapter.js +4 -3
  43. package/src/adapters/maria-adapter.js +33 -7
  44. package/src/adapters/mega-cache-adapter.js +83 -6
  45. package/src/adapters/mega-db-adapter.js +10 -1
  46. package/src/adapters/mongo-adapter.js +40 -8
  47. package/src/adapters/postgres-adapter.js +33 -6
  48. package/src/adapters/redis-adapter.js +7 -3
  49. package/src/adapters/sqlite-adapter.js +26 -3
  50. package/src/cli/commands/console-cmd.js +3 -1
  51. package/src/cli/commands/new.js +13 -3
  52. package/src/cli/commands/scaffold.js +173 -33
  53. package/src/cli/generators/index.js +140 -3
  54. package/src/cli/index.js +437 -155
  55. package/src/cli/watch.js +188 -0
  56. package/src/core/ajv-mapper.js +30 -3
  57. package/src/core/boot.js +464 -245
  58. package/src/core/cluster-metrics.js +13 -4
  59. package/src/core/ctx-builder.js +65 -3
  60. package/src/core/envelope.js +119 -12
  61. package/src/core/hub-link.js +89 -18
  62. package/src/core/i18n.js +11 -1
  63. package/src/core/index.js +7 -3
  64. package/src/core/mega-app.js +253 -505
  65. package/src/core/mega-cluster.js +4 -1
  66. package/src/core/mega-server.js +40 -9
  67. package/src/core/migration/dialect-registry.js +107 -0
  68. package/src/core/migration/dialects/README.md +62 -0
  69. package/src/core/migration/dialects/maria.js +496 -0
  70. package/src/core/migration/dialects/mongo.js +824 -0
  71. package/src/core/migration/dialects/postgres.js +563 -0
  72. package/src/core/migration/dialects/sqlite.js +476 -0
  73. package/src/core/migration/differ.js +456 -0
  74. package/src/core/migration/generate.js +508 -0
  75. package/src/core/migration/journal.js +167 -0
  76. package/src/core/migration/model-scan.js +84 -0
  77. package/src/core/migration/mongo-migration-db.js +97 -0
  78. package/src/core/migration/schema-builder.js +400 -0
  79. package/src/core/migration/schema-validator.js +315 -0
  80. package/src/core/migration-lock.js +205 -0
  81. package/src/core/migration-runner.js +166 -38
  82. package/src/core/multipart.js +28 -5
  83. package/src/core/pipeline.js +131 -0
  84. package/src/core/router.js +70 -65
  85. package/src/core/scope-registry.js +1 -0
  86. package/src/core/security.js +70 -12
  87. package/src/core/session-store.js +14 -1
  88. package/src/core/workers-manager.js +12 -1
  89. package/src/core/ws-cluster.js +10 -3
  90. package/src/core/ws-message.js +48 -4
  91. package/src/core/ws-presence.js +636 -0
  92. package/src/core/ws-roster.js +50 -8
  93. package/src/core/ws-upgrade.js +223 -12
  94. package/src/index.js +1 -1
  95. package/src/lib/hub-protocol.js +29 -0
  96. package/src/lib/mega-circuit-breaker.js +5 -3
  97. package/src/lib/mega-health.js +35 -4
  98. package/src/lib/mega-job-queue.js +151 -34
  99. package/src/lib/mega-job.js +37 -1
  100. package/src/lib/mega-metrics.js +31 -13
  101. package/src/lib/mega-plugin.js +34 -3
  102. package/src/lib/mega-schedule.js +40 -22
  103. package/src/lib/mega-shutdown.js +114 -39
  104. package/src/lib/mega-tracing.js +66 -19
  105. package/src/lib/mega-worker.js +33 -6
  106. package/src/lib/otel-resource.js +36 -0
  107. package/src/{cli → lib}/ws-hub.js +139 -15
  108. package/src/models/crud-sql-builder.js +133 -0
  109. package/src/models/mega-model.js +82 -2
  110. package/src/models/model-crud.js +483 -0
  111. package/src/models/mongo-crud.js +285 -0
  112. package/templates/adr/code.tpl +23 -0
  113. package/templates/model/code-mongo.tpl +35 -0
  114. package/templates/model/code.tpl +15 -1
  115. package/templates/model/test-mongo.tpl +38 -0
  116. package/templates/model/test.tpl +4 -0
  117. package/types/adapters/adapter-manager.d.ts +95 -0
  118. package/types/adapters/adapter-options.d.ts +93 -0
  119. package/types/adapters/file-adapter.d.ts +105 -0
  120. package/types/adapters/file-session-adapter.d.ts +103 -0
  121. package/types/adapters/index.d.ts +20 -0
  122. package/types/adapters/maria-adapter.d.ts +117 -0
  123. package/types/adapters/mega-adapter.d.ts +215 -0
  124. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  125. package/types/adapters/mega-cache-adapter.d.ts +73 -0
  126. package/types/adapters/mega-db-adapter.d.ts +50 -0
  127. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  128. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  129. package/types/adapters/mega-session-adapter.d.ts +32 -0
  130. package/types/adapters/mongo-adapter.d.ts +150 -0
  131. package/types/adapters/nats-adapter.d.ts +108 -0
  132. package/types/adapters/postgres-adapter.d.ts +141 -0
  133. package/types/adapters/redis-adapter.d.ts +78 -0
  134. package/types/adapters/redis-session-adapter.d.ts +82 -0
  135. package/types/adapters/redlock-adapter.d.ts +149 -0
  136. package/types/adapters/registry.d.ts +46 -0
  137. package/types/adapters/sqlite-adapter.d.ts +112 -0
  138. package/types/auth/index.d.ts +24 -0
  139. package/types/cli/commands/console-cmd.d.ts +37 -0
  140. package/types/cli/commands/new.d.ts +16 -0
  141. package/types/cli/commands/routes.d.ts +36 -0
  142. package/types/cli/commands/scaffold.d.ts +78 -0
  143. package/types/cli/commands/test-cmd.d.ts +14 -0
  144. package/types/cli/generators/index.d.ts +122 -0
  145. package/types/cli/index.d.ts +234 -0
  146. package/types/cli/template-engine.d.ts +40 -0
  147. package/types/cli/watch.d.ts +59 -0
  148. package/types/core/ajv-mapper.d.ts +27 -0
  149. package/types/core/boot.d.ts +233 -0
  150. package/types/core/cluster-metrics.d.ts +52 -0
  151. package/types/core/config-loader.d.ts +13 -0
  152. package/types/core/config-validator.d.ts +30 -0
  153. package/types/core/ctx-builder.d.ts +103 -0
  154. package/types/core/envelope.d.ts +79 -0
  155. package/types/core/error-mapper.d.ts +17 -0
  156. package/types/core/formbody.d.ts +41 -0
  157. package/types/core/hub-link.d.ts +266 -0
  158. package/types/core/i18n.d.ts +178 -0
  159. package/types/core/index.d.ts +28 -0
  160. package/types/core/mega-app.d.ts +529 -0
  161. package/types/core/mega-cluster.d.ts +104 -0
  162. package/types/core/mega-server.d.ts +91 -0
  163. package/types/core/mega-service.d.ts +31 -0
  164. package/types/core/migration/dialect-registry.d.ts +22 -0
  165. package/types/core/migration/dialects/maria.d.ts +99 -0
  166. package/types/core/migration/dialects/mongo.d.ts +89 -0
  167. package/types/core/migration/dialects/postgres.d.ts +117 -0
  168. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  169. package/types/core/migration/differ.d.ts +47 -0
  170. package/types/core/migration/generate.d.ts +56 -0
  171. package/types/core/migration/journal.d.ts +52 -0
  172. package/types/core/migration/model-scan.d.ts +19 -0
  173. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  174. package/types/core/migration/schema-builder.d.ts +197 -0
  175. package/types/core/migration/schema-validator.d.ts +20 -0
  176. package/types/core/migration-lock.d.ts +33 -0
  177. package/types/core/migration-runner.d.ts +101 -0
  178. package/types/core/multipart.d.ts +86 -0
  179. package/types/core/openapi.d.ts +62 -0
  180. package/types/core/pipeline.d.ts +93 -0
  181. package/types/core/router.d.ts +159 -0
  182. package/types/core/routes-loader.d.ts +21 -0
  183. package/types/core/scope-registry.d.ts +14 -0
  184. package/types/core/security.d.ts +77 -0
  185. package/types/core/services-loader.d.ts +27 -0
  186. package/types/core/session-cleanup-schedule.d.ts +19 -0
  187. package/types/core/session-store.d.ts +25 -0
  188. package/types/core/session.d.ts +77 -0
  189. package/types/core/static-assets.d.ts +73 -0
  190. package/types/core/template.d.ts +106 -0
  191. package/types/core/workers-manager.d.ts +79 -0
  192. package/types/core/ws-cluster.d.ts +208 -0
  193. package/types/core/ws-compression.d.ts +112 -0
  194. package/types/core/ws-controller.d.ts +65 -0
  195. package/types/core/ws-message.d.ts +106 -0
  196. package/types/core/ws-presence.d.ts +273 -0
  197. package/types/core/ws-roster.d.ts +108 -0
  198. package/types/core/ws-upgrade.d.ts +260 -0
  199. package/types/errors/config-error.d.ts +10 -0
  200. package/types/errors/http-errors.d.ts +120 -0
  201. package/types/errors/index.d.ts +3 -0
  202. package/types/errors/mega-error.d.ts +32 -0
  203. package/types/index.d.ts +39 -0
  204. package/types/lib/asp/config.d.ts +49 -0
  205. package/types/lib/asp/crypto.d.ts +43 -0
  206. package/types/lib/asp/errors.d.ts +30 -0
  207. package/types/lib/asp/nonce-cache.d.ts +52 -0
  208. package/types/lib/asp/plugin.d.ts +30 -0
  209. package/types/lib/asp/ws-terminator.d.ts +45 -0
  210. package/types/lib/env-mapper.d.ts +14 -0
  211. package/types/lib/hub-protocol.d.ts +106 -0
  212. package/types/lib/index.d.ts +22 -0
  213. package/types/lib/logger/telegram-core.d.ts +104 -0
  214. package/types/lib/logger/telegram-transport.d.ts +45 -0
  215. package/types/lib/mega-brute-force.d.ts +66 -0
  216. package/types/lib/mega-circuit-breaker.d.ts +243 -0
  217. package/types/lib/mega-cron.d.ts +66 -0
  218. package/types/lib/mega-hash.d.ts +32 -0
  219. package/types/lib/mega-health.d.ts +48 -0
  220. package/types/lib/mega-job-queue.d.ts +188 -0
  221. package/types/lib/mega-job-worker.d.ts +130 -0
  222. package/types/lib/mega-job.d.ts +145 -0
  223. package/types/lib/mega-logger.d.ts +45 -0
  224. package/types/lib/mega-metrics.d.ts +285 -0
  225. package/types/lib/mega-plugin.d.ts +245 -0
  226. package/types/lib/mega-retry.d.ts +85 -0
  227. package/types/lib/mega-schedule.d.ts +260 -0
  228. package/types/lib/mega-shutdown.d.ts +135 -0
  229. package/types/lib/mega-tracing.d.ts +224 -0
  230. package/types/lib/mega-worker.d.ts +129 -0
  231. package/types/lib/otel-resource.d.ts +16 -0
  232. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  233. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  234. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  235. package/types/lib/ws-hub.d.ts +259 -0
  236. package/types/models/crud-sql-builder.d.ts +48 -0
  237. package/types/models/index.d.ts +1 -0
  238. package/types/models/mega-model.d.ts +138 -0
  239. package/types/models/model-crud.d.ts +82 -0
  240. package/types/models/mongo-crud.d.ts +59 -0
  241. package/types/test/index.d.ts +84 -0
  242. package/.env +0 -127
  243. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  244. package/sample/crud/apps/main/models/note.js +0 -71
  245. package/sample/crud/apps/main/models/user.js +0 -86
  246. package/sample/crud/package-lock.json +0 -5665
  247. package/sample/crud/yarn.lock +0 -2142
  248. package/sample/simple/package-lock.json +0 -1851
@@ -3,33 +3,60 @@
3
3
  /**
4
4
  * MegaShutdown — graceful shutdown 시퀀스 + 사용자 cleanup hook 묶음.
5
5
  *
6
- * 시퀀스 (docs/10 §3):
7
- * Running → DrainingReady (health 'ready' = 503)
8
- * → ClosingHttp (Fastify close, 진행중 요청 grace)
9
- * → ClosingWs (placeholder)
10
- * → ClosingJobs / ClosingScheduler (placeholder)
11
- * → DisconnectingAdapters (placeholder)
12
- * → FlushingLogs (placeholder)
13
- * → Exited (process.exit(0))
6
+ * 시퀀스 (docs/10 §3) — 명시 stage 로 코드화({@link SHUTDOWN_STAGES}):
7
+ * Running → [server jobs app → workers → adapters → telemetry → logs] → Exited (process.exit)
14
8
  *
15
- * SIGTERM/SIGINT 캐치 + 사용자 hook 실행 (LIFO) + grace period + hardKill.
16
- * 후속 Phase 에서 단계 hook 자동 추가 (Fastify·어댑터·로거 등).
9
+ * SIGTERM/SIGINT 캐치 + stage 순서 실행(stage 안은 등록 역순 LIFO) + hook 별 grace + hardKill 데드라인.
10
+ * readiness 503 전환(DrainingReady)은 isShuttingDown() MegaHealth.checkAll 읽어 즉시 반영된다.
17
11
  *
18
12
  * API 는 docs/10·07 시퀀스에 맞춰 `MegaShutdown` 객체로 통일 (ADR — M-4).
19
13
  *
20
14
  * 사용 예:
21
- * MegaShutdown.register('http', async () => server.close())
22
- * MegaShutdown.register('db', async () => pool.end())
15
+ * MegaShutdown.register('my-queue', async () => consumer.stop()) // 기본 stage 'app'
16
+ * MegaShutdown.register('db', async () => pool.end(), { stage: 'adapters' }) // 명시 stage
23
17
  * MegaShutdown.setupSignals({ gracePeriodMs: 30_000, hardKillMs: 60_000 })
24
18
  * await MegaShutdown.now() // 수동 트리거
25
19
  */
26
20
 
27
- /** @type {Array<{ name: string, fn: () => Promise<void> | void }>} */
21
+ /**
22
+ * 종료 stage 정본 순서 (docs/10 §3 의 단계를 코드로 명시) — `register(name, fn, { stage })` 가 이 중
23
+ * 하나를 지정하고, `now()` 는 이 배열 순서대로 stage 를 실행한다(stage 안에서는 등록 역순 LIFO).
24
+ *
25
+ * server — HTTP/WS 수용 종료(서버 close, 진행 중 요청 drain). ClosingHttp/ClosingWs.
26
+ * jobs — 잡 컨슈머·스케줄러 정지(새 잡 수신 중단, 진행 중 잡 완료). ClosingJobs/ClosingScheduler.
27
+ * app — 앱 레벨 정리(플러그인 beforeShutdown·세션 store·WS cluster/roster 등 어댑터를 **쓰는** 정리).
28
+ * `stage` 미지정 기본값 — 사용자 cleanup 은 어댑터가 살아 있는 이 단계에서 돈다.
29
+ * workers — CPU 워커 풀·embedded wsHub 등 백그라운드 실행기 정지.
30
+ * adapters — 공유 어댑터 disconnect(DisconnectingAdapters, stage 안 LIFO = connect 역순).
31
+ * telemetry — 메트릭·트레이싱 SDK flush/shutdown. 어댑터 **뒤** — disconnect 까지의 span/메트릭을 내보낸다.
32
+ * logs — 로거 flush(FlushingLogs). 항상 마지막 — 종료 시퀀스 자체의 로그가 유실되지 않게.
33
+ */
34
+ export const SHUTDOWN_STAGES = Object.freeze(['server', 'jobs', 'app', 'workers', 'adapters', 'telemetry', 'logs'])
35
+
36
+ /** `register` 가 stage 미지정일 때 들어가는 기본 stage — 사용자 cleanup 의 표준 위치(어댑터 종료 전). */
37
+ const DEFAULT_STAGE = 'app'
38
+
39
+ /** @type {Array<{ name: string, fn: () => Promise<void> | void, stage: string }>} */
28
40
  const handlers = []
29
41
  let signalsRegistered = false
42
+ /** @type {Array<{ sig: string, fn: () => void }>} setupSignals 가 등록한 시그널 리스너(_reset 해제용). */
43
+ let signalHandlers = []
30
44
  let isShuttingDownFlag = false
31
- let gracePeriodMs = 30_000
32
- let hardKillMs = 60_000
45
+
46
+ /** hook 1개당 grace 기본값(ms) — 30초. `setupSignals({ gracePeriodMs })` 로 조정. */
47
+ export const DEFAULT_GRACE_PERIOD_MS = 30_000
48
+
49
+ /**
50
+ * graceful shutdown 전체 상한 기본값(ms) — 60초. 초과 시 hardKill(exit 1). 워커 프로세스의 종료 예산
51
+ * 상한이므로, 클러스터 마스터의 grace(`MegaCluster.gracePeriodMs`)는 **이 값 + 마진 이상**이어야
52
+ * 워커가 drain 을 끝내기 전에 마스터가 SIGKILL 하는 예산 역전이 없다(CLI 가 이 상수로 위계화).
53
+ */
54
+ export const DEFAULT_HARD_KILL_MS = 60_000
55
+
56
+ let gracePeriodMs = DEFAULT_GRACE_PERIOD_MS
57
+ let hardKillMs = DEFAULT_HARD_KILL_MS
58
+ /** hook 루프가 hardKill 직전에 끝나도록 남기는 여유(ms) — 루프가 hardKill 타이머를 clear 하고 정상 exit 한다. */
59
+ const HARD_KILL_MARGIN_MS = 100
33
60
  let exitedHandlers = new Set()
34
61
  /** 전역 에러 핸들러 등록 여부 + 참조(reset 시 removeListener 용). @type {boolean} */
35
62
  let globalErrorsRegistered = false
@@ -47,23 +74,30 @@ let uncaughtExceptionHandler = null
47
74
  let shutdownLogger = null
48
75
 
49
76
  /**
50
- * cleanup hook 등록. 순서는 등록 역순(LIFO)으로 실행.
51
- * @param {string} name - 식별자 (로그용)
77
+ * cleanup hook 등록. 실행 순서 = {@link SHUTDOWN_STAGES} 순서 → 같은 stage 안에서는 등록 역순(LIFO).
78
+ * `stage` 미지정이면 `'app'`(어댑터 종료 전, 사용자 cleanup 표준 위치) — 기존 2-인자 호출과 호환.
79
+ * @param {string} name - 식별자 (로그·unregister 용)
52
80
  * @param {() => Promise<void> | void} fn
81
+ * @param {{ stage?: string }} [opts] - 종료 stage({@link SHUTDOWN_STAGES} 중 하나).
53
82
  */
54
- function register(name, fn) {
83
+ function register(name, fn, opts = {}) {
55
84
  if (typeof name !== 'string' || name.length === 0) {
56
85
  throw new Error('MegaShutdown.register: name is required (string)')
57
86
  }
58
87
  if (typeof fn !== 'function') {
59
88
  throw new Error('MegaShutdown.register: fn must be a function')
60
89
  }
61
- handlers.push({ name, fn })
90
+ const stage = opts.stage ?? DEFAULT_STAGE
91
+ // stage 오타가 조용히 엉뚱한 순서로 실행되지 않게 등록 시점 fail-fast.
92
+ if (!SHUTDOWN_STAGES.includes(stage)) {
93
+ throw new Error(`MegaShutdown.register: unknown stage '${stage}' (valid: ${SHUTDOWN_STAGES.join(', ')})`)
94
+ }
95
+ handlers.push({ name, fn, stage })
62
96
  }
63
97
 
64
98
  /**
65
99
  * 등록된 cleanup hook 제거 (같은 name 전부). 런타임 중 동적으로 붙였다 떼는 hook(예: hub link)
66
- * 의 누수를 막는다(L1). shutdown 진행 중에는 인덱스 추적(exitedHandlers)과 충돌하므로 무시한다.
100
+ * 의 누수를 막는다(L1). shutdown 진행 중에는 실행 추적(exitedHandlers)과 충돌하므로 무시한다.
67
101
  * @param {string} name - register 시 쓴 식별자.
68
102
  * @returns {number} 제거된 hook 수.
69
103
  */
@@ -99,9 +133,13 @@ function setupSignals(opts = {}) {
99
133
  if (typeof opts.hardKillMs === 'number') hardKillMs = opts.hardKillMs
100
134
  const signals = opts.signals ?? ['SIGTERM', 'SIGINT']
101
135
  for (const sig of signals) {
102
- process.on(sig, () => {
136
+ // 리스너 참조를 보관해 _reset 이 떼어낼 수 있게 한다 — reset 후 재-setup 시 리스너가 중복돼
137
+ // 시그널 1회에 now() 가 2회 불리면 두 번째가 M-3(즉시 force exit 1)로 오인된다.
138
+ const fn = () => {
103
139
  void now({ signal: sig, exitCode: 0 })
104
- })
140
+ }
141
+ signalHandlers.push({ sig, fn })
142
+ process.on(sig, fn)
105
143
  }
106
144
  // 전역 에러 핸들러도 함께 등록(opt-out: globalErrorHandlers === false). REPL(console-cmd) 등은 끌 수 있다.
107
145
  if (opts.globalErrorHandlers !== false) setupGlobalErrorHandlers()
@@ -118,12 +156,23 @@ function setupSignals(opts = {}) {
118
156
  function setupGlobalErrorHandlers({ exitCode = 1 } = {}) {
119
157
  if (globalErrorsRegistered) return
120
158
  globalErrorsRegistered = true
159
+ // shutdown 진행 중의 에러는 now() 를 재호출하지 않는다 — teardown 구간(소켓·어댑터 정리)은 floating
160
+ // rejection 이 가장 나기 쉬운데, 여기서 now() 를 다시 부르면 "두 번째 시그널"(M-3)로 오인돼 남은
161
+ // hook(어댑터 disconnect·로그 flush)을 건너뛰고 즉시 exit(1) 된다. fatal 기록만 하고 정리를 계속한다.
121
162
  unhandledRejectionHandler = (reason) => {
122
163
  const err = reason instanceof Error ? reason : new Error(`unhandledRejection: ${String(reason)}`)
164
+ if (isShuttingDownFlag) {
165
+ logShutdown('fatal', 'unhandledRejection during shutdown (continuing cleanup)', { err })
166
+ return
167
+ }
123
168
  logShutdown('fatal', 'unhandledRejection — initiating graceful shutdown', { err })
124
169
  void now({ signal: 'unhandledRejection', exitCode })
125
170
  }
126
171
  uncaughtExceptionHandler = (err, origin) => {
172
+ if (isShuttingDownFlag) {
173
+ logShutdown('fatal', 'uncaughtException during shutdown (continuing cleanup)', { err, origin })
174
+ return
175
+ }
127
176
  logShutdown('fatal', 'uncaughtException — initiating graceful shutdown', { err, origin })
128
177
  void now({ signal: 'uncaughtException', exitCode })
129
178
  }
@@ -173,30 +222,49 @@ async function now({ signal, exitCode = 0 } = {}) {
173
222
  logShutdown('info', 'shutdown starting', { signal: signal ?? 'manual', handlers: handlers.length, graceMs: gracePeriodMs })
174
223
 
175
224
  // hardKill 보호 — hardKillMs 초과 시 강제 종료
225
+ const hardKillAt = Date.now() + hardKillMs
176
226
  const hardKillTimer = setTimeout(() => {
177
227
  logShutdown('error', 'grace period exceeded — force exit(1)', { hardKillMs })
178
228
  process.exit(1)
179
229
  }, hardKillMs)
180
230
  hardKillTimer.unref()
181
231
 
182
- // LIFO 순서로 hook 실행 (last registered runs first)
183
- for (let i = handlers.length - 1; i >= 0; i--) {
184
- const { name, fn } = handlers[i]
185
- if (exitedHandlers.has(i)) continue
186
- try {
187
- logShutdown('info', `running hook '${name}'`, { hook: name })
188
- await Promise.race([
189
- Promise.resolve(fn()),
190
- new Promise((_, reject) =>
191
- setTimeout(() => reject(new Error('handler timeout')), gracePeriodMs).unref(),
192
- ),
193
- ])
194
- exitedHandlers.add(i)
195
- logShutdown('info', `hook '${name}' done`, { hook: name })
196
- } catch (err) {
197
- // silent 금지. 핸들러 실패 시 warn 로그 + 다음 hook 진행.
198
- logShutdown('warn', `hook '${name}' failed (continuing)`, { hook: name, err: /** @type {any} */ (err)?.message ?? err })
232
+ // stage 순서(SHUTDOWN_STAGES)대로 실행, 같은 stage 안에서는 등록 역순(LIFO).
233
+ // 실패 격리는 2단 hook 실패는 warn 같은 stage 의 다음 hook 으로, stage 의 실패 수는
234
+ // stage done 로그에 집계된다(한 stage 가 망가져도 다음 stage 는 항상 진행).
235
+ for (const stage of SHUTDOWN_STAGES) {
236
+ /** @type {Array<{ name: string, fn: () => Promise<void> | void, stage: string }>} */
237
+ const staged = []
238
+ for (let i = handlers.length - 1; i >= 0; i--) {
239
+ if (handlers[i].stage === stage && !exitedHandlers.has(handlers[i])) staged.push(handlers[i])
199
240
  }
241
+ if (staged.length === 0) continue
242
+ const stageStartedAt = Date.now()
243
+ logShutdown('info', `stage '${stage}' starting`, { stage, hooks: staged.length })
244
+ let failed = 0
245
+ for (const entry of staged) {
246
+ const { name, fn } = entry
247
+ // hook 별 대기 예산 — gracePeriodMs 를 기본으로 하되 hardKill 데드라인을 넘기지 않게 남은
248
+ // 시간으로 캡한다(여유 HARD_KILL_MARGIN_MS). 앞 hook/stage 들이 grace 를 소진해도 뒤 hook 이
249
+ // "시작조차 못 한 채" hardKill 로 잘리지 않고, 루프가 데드라인 전에 끝나 정상 exit 한다.
250
+ const budgetMs = Math.max(0, Math.min(gracePeriodMs, hardKillAt - HARD_KILL_MARGIN_MS - Date.now()))
251
+ try {
252
+ logShutdown('info', `running hook '${name}'`, { hook: name, stage })
253
+ await Promise.race([
254
+ Promise.resolve(fn()),
255
+ new Promise((_, reject) =>
256
+ setTimeout(() => reject(new Error('handler timeout')), budgetMs).unref(),
257
+ ),
258
+ ])
259
+ exitedHandlers.add(entry)
260
+ logShutdown('info', `hook '${name}' done`, { hook: name, stage })
261
+ } catch (err) {
262
+ // silent 금지. 핸들러 실패 시 warn 로그 + 다음 hook 진행.
263
+ failed++
264
+ logShutdown('warn', `hook '${name}' failed (continuing)`, { hook: name, stage, err: /** @type {any} */ (err)?.message ?? err })
265
+ }
266
+ }
267
+ logShutdown('info', `stage '${stage}' done`, { stage, tookMs: Date.now() - stageStartedAt, failed })
200
268
  }
201
269
 
202
270
  clearTimeout(hardKillTimer)
@@ -227,6 +295,12 @@ function registeredCount() {
227
295
  */
228
296
  function _reset() {
229
297
  handlers.length = 0
298
+ // 시그널 리스너도 떼어낸다 — 남겨두면 reset 후 재-setup 시 리스너가 누적돼 시그널 1회에
299
+ // now() 가 2회 불리고, 두 번째가 M-3(즉시 force exit 1)로 처리된다.
300
+ for (const { sig, fn } of signalHandlers) {
301
+ process.removeListener(/** @type {any} */ (sig), fn)
302
+ }
303
+ signalHandlers = []
230
304
  signalsRegistered = false
231
305
  isShuttingDownFlag = false
232
306
  gracePeriodMs = 30_000
@@ -246,6 +320,7 @@ function _reset() {
246
320
  * docs/10·07 시퀀스가 `MegaShutdown.now()` 형태이므로 객체로 통일 (M-4).
247
321
  */
248
322
  export const MegaShutdown = {
323
+ STAGES: SHUTDOWN_STAGES,
249
324
  register,
250
325
  unregister,
251
326
  isShuttingDown,
@@ -21,14 +21,23 @@
21
21
  * `enterWith` 방식이 공유 동기 실행을 오염시켜 sibling 을 잘못 묶던 문제(스모크로 확인)를 구조적으로
22
22
  * 제거한다. 이 seam 은 ADR-077 의 `_instrument` 에 추가된 **단 한 줄(run 위임)** 이다(ADR-114, 오너 결정).
23
23
  *
24
- * # 신규 의존성 (ADR-114 + ADR-126 보강)
24
+ * # W3C trace context 전파 (ADR-196 F5 audit O-1)
25
+ * 서비스 경계를 넘는 분산 추적: inbound 는 {@link extractRemoteContext} 가 `traceparent`/`tracestate`
26
+ * 헤더를 부모 컨텍스트로 복원해 HTTP 루트 span 이 게이트웨이/업스트림 trace 에 이어지고, outbound 는
27
+ * {@link propagationHeaders}(= `ctx.tracer.propagationHeaders()`)가 현재 활성 span 을 헤더로 직렬화해
28
+ * 하류 HTTP 호출(fetch 등)에 싣는다. 포맷·파싱은 검증된 `W3CTraceContextPropagator`(@opentelemetry/core)
29
+ * 에 위임한다(P1 — 직접 파싱 금지). 무효 헤더는 무시(새 루트 — fail-safe), 옵트인 OFF 면 0 비용.
30
+ *
31
+ * # 신규 의존성 (ADR-114 + ADR-126 보강 + ADR-196)
25
32
  * `@opentelemetry/api`, `@opentelemetry/sdk-trace-base`, `@opentelemetry/resources`,
26
33
  * `@opentelemetry/semantic-conventions`, `@opentelemetry/exporter-trace-otlp-http`(OTLP exporter),
27
- * `@opentelemetry/exporter-zipkin`(Zipkin exporter, ADR-126 보강 — 사용자 승인). audit 신규 0건.
34
+ * `@opentelemetry/exporter-zipkin`(Zipkin exporter, ADR-126 보강 — 사용자 승인),
35
+ * `@opentelemetry/core`(W3C propagator, ADR-196 — 사용자 승인. 기존 2.x 라인 transitive 의 직접 승격).
28
36
  *
29
37
  * @module lib/mega-tracing
30
38
  */
31
- import { trace, ROOT_CONTEXT, SpanKind, SpanStatusCode } from '@opentelemetry/api'
39
+ import { trace, ROOT_CONTEXT, SpanKind, SpanStatusCode, isSpanContextValid, defaultTextMapGetter, defaultTextMapSetter } from '@opentelemetry/api'
40
+ import { W3CTraceContextPropagator } from '@opentelemetry/core'
32
41
  import {
33
42
  BasicTracerProvider,
34
43
  SimpleSpanProcessor,
@@ -40,14 +49,8 @@ import {
40
49
  TraceIdRatioBasedSampler,
41
50
  ParentBasedSampler,
42
51
  } from '@opentelemetry/sdk-trace-base'
43
- import { resourceFromAttributes } from '@opentelemetry/resources'
44
- import {
45
- ATTR_SERVICE_NAME,
46
- ATTR_SERVICE_VERSION,
47
- ATTR_DB_SYSTEM_NAME,
48
- ATTR_DB_QUERY_TEXT,
49
- ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
50
- } from '@opentelemetry/semantic-conventions'
52
+ import { ATTR_DB_SYSTEM_NAME, ATTR_DB_QUERY_TEXT } from '@opentelemetry/semantic-conventions'
53
+ import { buildOtelResource } from './otel-resource.js'
51
54
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
52
55
  import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
53
56
  import { AsyncLocalStorage } from 'node:async_hooks'
@@ -80,6 +83,48 @@ function getActiveContext() {
80
83
  return activeContextStore.getStore() ?? ROOT_CONTEXT
81
84
  }
82
85
 
86
+ /** W3C trace context propagator(무상태) — traceparent/tracestate 파싱·직렬화 정본 구현(ADR-196). */
87
+ const w3cPropagator = new W3CTraceContextPropagator()
88
+
89
+ /**
90
+ * inbound 헤더의 `traceparent`(/`tracestate`)를 부모 OTel Context 로 복원한다(ADR-196, W3C trace context).
91
+ *
92
+ * 게이트웨이/업스트림이 보낸 trace 에 HTTP 루트 span 을 잇는 입구다 — {@link enterHttpSpan} 의 `headers`
93
+ * 옵션이 이 함수를 거친다. 헤더가 없거나 형식이 무효면 `undefined`(호출부가 새 루트로 시작 — fail-safe,
94
+ * 잘못된 외부 입력이 추적을 깨지 않게). 옵트인 OFF 면 `undefined`(0 비용).
95
+ *
96
+ * 비율 샘플링(`traceidratio`)은 `ParentBasedSampler` 라 복원된 원격 부모의 sampled flag 를 따른다 —
97
+ * 업스트림이 샘플한 trace 는 여기서도 기록되고, 버린 trace 는 여기서도 버려져 trace 가 반토막 나지 않는다.
98
+ *
99
+ * @param {Record<string, string | string[] | undefined>} headers - `req.headers`(대소문자 무관 키).
100
+ * @returns {import('@opentelemetry/api').Context | undefined} 유효한 원격 부모 컨텍스트, 없으면 undefined.
101
+ */
102
+ export function extractRemoteContext(headers) {
103
+ if (state === null) return undefined
104
+ if (!headers || typeof headers !== 'object') return undefined
105
+ const ctx = w3cPropagator.extract(ROOT_CONTEXT, headers, defaultTextMapGetter)
106
+ const sc = trace.getSpanContext(ctx)
107
+ if (!sc || !isSpanContextValid(sc)) return undefined // 무효 traceparent — 새 루트로(fail-safe).
108
+ return ctx
109
+ }
110
+
111
+ /**
112
+ * 현재 활성 span 을 outbound 헤더(`traceparent`/`tracestate`)로 직렬화해 carrier 에 채운다(ADR-196).
113
+ *
114
+ * 하류 HTTP 호출에 trace 를 잇는 출구다 — `fetch(url, { headers: ctx.tracer.propagationHeaders() })`.
115
+ * 활성 span 이 없거나 옵트인 OFF 면 carrier 를 그대로 반환(주입 없음 — 헤더 오염 없음).
116
+ *
117
+ * @param {Record<string, string>} [carrier={}] - 채울 헤더 객체(기존 키 보존, traceparent/tracestate 만 추가).
118
+ * @returns {Record<string, string>} carrier(체이닝용 동일 객체).
119
+ * @example
120
+ * const res = await fetch(downstreamUrl, { headers: MegaTracing.propagationHeaders({ accept: 'application/json' }) })
121
+ */
122
+ export function propagationHeaders(carrier = {}) {
123
+ if (state === null) return carrier
124
+ w3cPropagator.inject(getActiveContext(), carrier, defaultTextMapSetter)
125
+ return carrier
126
+ }
127
+
83
128
  /**
84
129
  * 비활성(옵트인 OFF) 시 사용자 코드에 넘길 **no-op span** — 메서드는 다 있지만 아무것도 기록 안 함.
85
130
  * `ctx.tracer.span(name, (span) => span.setAttribute(...))` 가 OFF 에서도 깨지지 않게 한다.
@@ -169,12 +214,8 @@ export function init(opts = /** @type {any} */ ({})) {
169
214
  const { exporter, processorKind } = buildExporter(opts)
170
215
  const SpanProcessor = processorKind === 'batch' ? BatchSpanProcessor : SimpleSpanProcessor
171
216
 
172
- const resource = resourceFromAttributes({
173
- [ATTR_SERVICE_NAME]: serviceName,
174
- ...(typeof opts.version === 'string' ? { [ATTR_SERVICE_VERSION]: opts.version } : {}),
175
- ...(typeof opts.environment === 'string' ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: opts.environment } : {}),
176
- ...(opts.attributes && typeof opts.attributes === 'object' ? opts.attributes : {}),
177
- })
217
+ // resource 조립은 메트릭과 공유하는 단일 출처(otel-resource.js, ADR-196) — 두 SDK 간 드리프트 방지.
218
+ const resource = buildOtelResource({ serviceName, version: opts.version, environment: opts.environment, attributes: opts.attributes })
178
219
 
179
220
  const provider = new BasicTracerProvider({
180
221
  resource,
@@ -411,13 +452,17 @@ export function enterSpan(name, opts = {}) {
411
452
  * 기록(`setError`), `onResponse` 에서 상태코드와 함께 닫는다(`finish`). HTTP 시맨틱(5xx=ERROR)을 한곳에
412
453
  * 모아 배선 코드(mega-app)가 OTel enum 을 안 만지게 한다. 옵트인 OFF 면 no-op 핸들.
413
454
  *
414
- * @param {{ method: string, route: string, path: string, host?: string, app: string }} info
455
+ * `headers` 주면 inbound `traceparent` 부모로 복원해({@link extractRemoteContext}, ADR-196) 루트
456
+ * span 이 업스트림 trace 에 이어진다 — 무효/부재면 종전대로 새 루트.
457
+ *
458
+ * @param {{ method: string, route: string, path: string, host?: string, app: string, headers?: Record<string, string | string[] | undefined> }} info
415
459
  * @returns {{ span: import('@opentelemetry/api').Span, setError: (err: unknown) => void, finish: (statusCode: number) => void }}
416
460
  */
417
- export function enterHttpSpan({ method, route, path, host, app }) {
461
+ export function enterHttpSpan({ method, route, path, host, app, headers }) {
418
462
  if (state === null) return /** @type {any} */ (NOOP_HTTP_HANDLE)
419
463
  const handle = enterSpan(`http.${method} ${route}`, {
420
464
  kind: SpanKind.SERVER,
465
+ parent: extractRemoteContext(headers ?? {}),
421
466
  attributes: {
422
467
  'http.request.method': method,
423
468
  'http.route': route,
@@ -476,6 +521,8 @@ export function logMixin() {
476
521
  */
477
522
  export const tracer = Object.freeze({
478
523
  span,
524
+ /** outbound 헤더에 traceparent/tracestate 주입(ADR-196) — `fetch(url, { headers: ctx.tracer.propagationHeaders() })`. */
525
+ propagationHeaders,
479
526
  /** @returns {import('@opentelemetry/api').Span | undefined} 현재 활성 span(없으면 undefined — 03-api-spec §10). */
480
527
  activeSpan() {
481
528
  if (state === null) return undefined
@@ -16,7 +16,9 @@
16
16
  * 프로세스 메모리 격리), process=`child_process.fork`(완전 격리, 더 무거움). 둘 다 node 빌트인(의존성 0).
17
17
  * - **풀 정책** — `static poolSize`(디폴트 `os.cpus().length - 1`, 최소 1). 작업 큐 + 가용 워커에 디스패치.
18
18
  * - **crash 자동 재시작** — 워커가 예기치 않게 죽으면 in-flight task 를 `worker.crashed` 로 reject 하고
19
- * `static maxRestarts`(디폴트 5)까지 교체 워커를 띄운다(MegaJobWorker M-1 패턴 정합).
19
+ * `static maxRestarts`(디폴트 5)/`static restartWindowMs`(디폴트 60s) **슬라이딩 윈도우** 안에서만
20
+ * 교체 워커를 띄운다 — 윈도우 내 한도 초과 = crash-loop 으로 보고 포기(MegaCluster 정책 통일).
21
+ * 산발 crash(윈도우 밖)는 한도를 소모하지 않아 장수 프로세스의 풀이 영구 축소되지 않는다.
20
22
  * - **graceful shutdown** — `stop()`: 새 `run()` 거부 + 큐 대기분 `worker.stopped` reject + in-flight 완료
21
23
  * 대기(allSettled) → 워커 terminate. `MegaShutdown` 통합은 workers-manager 가 배선.
22
24
  *
@@ -57,6 +59,14 @@ const DEFAULT_POOL_SIZE = Math.max(1, os.cpus().length - 1)
57
59
  /** crash 시 풀 전체에서 허용하는 교체 워커 재시작 총량 디폴트. */
58
60
  const DEFAULT_MAX_RESTARTS = 5
59
61
 
62
+ /**
63
+ * crash 교체 판정 윈도우 기본값(ms) — 60초. `maxRestarts` 는 이 윈도우 **안의** 교체 횟수 상한이다
64
+ * (수명 누적이 아님). 누적 카운터면 몇 주 간격의 산발 crash 도 한도를 소모해 장수 프로세스의 풀이
65
+ * 영구 축소되다 `pool_exhausted` 로 죽는다 — crash-loop(즉시 연속 사망)만 막으면 되는 안전망이므로
66
+ * 시간 윈도우 판정이 맞다(MegaCluster 의 rapid-crash 슬라이딩 윈도우와 정책 통일).
67
+ */
68
+ const DEFAULT_RESTART_WINDOW_MS = 60_000
69
+
60
70
  /**
61
71
  * @typedef {object} WorkerHandle - 풀 안의 워커 1개 핸들.
62
72
  * @property {number} id - 핸들 식별자(로그/디버그).
@@ -94,7 +104,8 @@ export class MegaWorker extends EventEmitter {
94
104
  /** @type {boolean} */ #stopping = false
95
105
  /** @type {number} */ #nextTaskId = 1
96
106
  /** @type {number} */ #nextHandleId = 1
97
- /** @type {number} crash 교체 누적. */ #restarts = 0
107
+ /** @type {number[]} crash 교체 시각(epoch ms) 슬라이딩 윈도우 — restartWindowMs 밖은 판정 시 제거. */
108
+ #restartTimes = []
98
109
  /** @type {number} 진행 중인 교체 spawn 수(일시적 풀 0 상태에서 run 을 큐잉할지 판단). */ #pendingRespawns = 0
99
110
 
100
111
  /**
@@ -130,12 +141,18 @@ export class MegaWorker extends EventEmitter {
130
141
  return typeof p === 'number' ? p : DEFAULT_POOL_SIZE
131
142
  }
132
143
 
133
- /** @returns {number} crash 교체 허용 총량. */
144
+ /** @returns {number} 윈도우({@link MegaWorker#restartWindowMs}) 내 crash 교체 허용 횟수. */
134
145
  get maxRestarts() {
135
146
  const r = /** @type {any} */ (this.constructor).maxRestarts
136
147
  return Number.isInteger(r) && r >= 0 ? r : DEFAULT_MAX_RESTARTS
137
148
  }
138
149
 
150
+ /** @returns {number} crash 교체 판정 슬라이딩 윈도우(ms). `static restartWindowMs` 로 조정. */
151
+ get restartWindowMs() {
152
+ const w = /** @type {any} */ (this.constructor).restartWindowMs
153
+ return Number.isInteger(w) && w > 0 ? w : DEFAULT_RESTART_WINDOW_MS
154
+ }
155
+
139
156
  /** @returns {boolean} start() 후 stop() 전이면 true. */
140
157
  get isStarted() {
141
158
  return this.#started
@@ -479,7 +496,11 @@ export class MegaWorker extends EventEmitter {
479
496
 
480
497
  /** 비정상 exit 처리(정상 종료 code 0 는 stop 경로에서만 기대). @param {WorkerHandle} handle @param {number|null} code */
481
498
  #onExit(handle, code) {
482
- if (this.#stopping || handle.down) return // 종료 중 terminate 로 인한 exit 은 정상.
499
+ if (handle.down) return // terminate 로 인한 exit 은 정상(이중 발화 가드).
500
+ // stop() 진행 중 crash 도 #onWorkerDown 으로 보낸다 — in-flight task 를 reject(→ _notify)해야
501
+ // stop() 의 완료 대기(allSettled)가 풀린다. 예전엔 #stopping 이면 통째로 무시해 그 task 가 영영
502
+ // settle 되지 않아 stop() 이 hang 했다(상위 MegaShutdown 타임아웃으로만 회수). respawn/drain 은
503
+ // #scheduleRespawn/#drainIfDead 의 자체 #stopping 가드가 막으므로 종료 중 부작용이 없다.
483
504
  this.#onWorkerDown(handle, new Error(`worker exited unexpectedly (code ${code})`))
484
505
  }
485
506
 
@@ -507,8 +528,14 @@ export class MegaWorker extends EventEmitter {
507
528
  * @returns {void}
508
529
  */
509
530
  #scheduleRespawn() {
510
- if (this.#stopping || this.#restarts >= this.maxRestarts) return
511
- this.#restarts++
531
+ if (this.#stopping) return
532
+ // 슬라이딩 윈도우 판정 — 윈도우 밖 기록을 비우고 남은 횟수가 한도면 crash-loop 으로 보고 포기.
533
+ // (수명 누적이 아니라서 산발 crash 는 풀을 영구 축소시키지 않는다 — MegaCluster 정책 통일.)
534
+ const now = Date.now()
535
+ const windowStart = now - this.restartWindowMs
536
+ this.#restartTimes = this.#restartTimes.filter((t) => t >= windowStart)
537
+ if (this.#restartTimes.length >= this.maxRestarts) return
538
+ this.#restartTimes.push(now)
512
539
  this.#pendingRespawns++
513
540
  this.#spawn()
514
541
  .then((h) => {
@@ -0,0 +1,36 @@
1
+ // @ts-check
2
+ /**
3
+ * OTel Resource 공유 빌더 — 트레이싱·메트릭 SDK 의 단일 출처 (ADR-196, F5 audit O-SDK 단일화).
4
+ *
5
+ * `MegaTracing.init`/`MegaMetrics.init` 이 각자 동일한 resource 조립 블록을 들고 있어 service.name 류
6
+ * 시맨틱 키 매핑이 두 곳에서 드리프트할 수 있었다. 본 모듈이 그 조립을 한 곳으로 모은다 — 두 SDK 가
7
+ * 같은 입력(`serviceName`/`version`/`environment`/`attributes`)에 항상 같은 resource 속성을 낸다.
8
+ * (전면 NodeSDK 채택은 자동 instrumentation 의존이 끌려와 zero-dep 방침과 충돌 — 자체 일원화로 결정.)
9
+ *
10
+ * @module lib/otel-resource
11
+ */
12
+ import { resourceFromAttributes } from '@opentelemetry/resources'
13
+ import {
14
+ ATTR_SERVICE_NAME,
15
+ ATTR_SERVICE_VERSION,
16
+ ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
17
+ } from '@opentelemetry/semantic-conventions'
18
+
19
+ /**
20
+ * 공통 입력 → OTel Resource. 시크릿은 attributes 에 싣지 말 것(exporter 로 평문 전송됨).
21
+ *
22
+ * @param {object} opts
23
+ * @param {string} opts.serviceName - **필수**(호출부가 선검증). `service.name` 속성.
24
+ * @param {string} [opts.version] - `service.version`.
25
+ * @param {string} [opts.environment] - `deployment.environment.name`.
26
+ * @param {Record<string, any>} [opts.attributes] - 추가 resource 속성(머지).
27
+ * @returns {import('@opentelemetry/resources').Resource}
28
+ */
29
+ export function buildOtelResource({ serviceName, version, environment, attributes }) {
30
+ return resourceFromAttributes({
31
+ [ATTR_SERVICE_NAME]: serviceName,
32
+ ...(typeof version === 'string' ? { [ATTR_SERVICE_VERSION]: version } : {}),
33
+ ...(typeof environment === 'string' ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: environment } : {}),
34
+ ...(attributes && typeof attributes === 'object' ? attributes : {}),
35
+ })
36
+ }