mega-framework 0.1.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/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaTracing — OpenTelemetry 분산 트레이싱 **옵트인** 통합 (ADR-114).
|
|
4
|
+
*
|
|
5
|
+
* ADR-077 의 어댑터 관찰성 hook(`onCallStart`/`onCallEnd`) 위에 **자동 span** 을 얹는다. 코어·어댑터
|
|
6
|
+
* 코드는 한 줄도 바꾸지 않는다 — `MegaAdapter.addHookListener`로 외부에서 hook 발화를
|
|
7
|
+
* 구독해 span 으로 변환할 뿐이다. 옵트인 OFF(`init` 미호출)면 어댑터에 리스너가 안 붙어 **0 비용**.
|
|
8
|
+
*
|
|
9
|
+
* # 동작 한눈에
|
|
10
|
+
* 1. `init(opts)` — OTel TracerProvider/exporter/sampler 구성 + tracer 확보.
|
|
11
|
+
* 2. `attachToManager(MegaAdapterManager)` — 부팅된 모든 공유 어댑터에 hook 리스너 일괄 구독.
|
|
12
|
+
* (또는 `subscribe(adapter, meta)` 로 개별 어댑터 구독.)
|
|
13
|
+
* 3. 어댑터 도메인 호출(`query`/`get`/`acquire` …)마다 `onCallStart`→span 시작, `onCallEnd`→span 종료.
|
|
14
|
+
* 4. `shutdown()` — 리스너 해제 + span flush + provider 종료.
|
|
15
|
+
*
|
|
16
|
+
* # span 중첩(부모-자식) 추적 — 베이스 `_instrument` 의 `run()` seam (ADR-077 보강)
|
|
17
|
+
* `onCallStart` 이 **스코프 토큰**(여기선 OTel span context)을 반환하면, 베이스 `_instrument` 가
|
|
18
|
+
* 도메인 `fn` 을 `MegaAdapter.#callScope.run(token, fn)` 안에서 실행한다. 그래서 `fn` 내부의 중첩
|
|
19
|
+
* 호출은 `adapter._currentCallScope` 로 부모 토큰을 읽어 자기 span 의 부모로 삼는다. `run()` 은
|
|
20
|
+
* 각 호출을 독립 스코프로 격리하므로 **순차·중첩·동시(`Promise.all`) 전부 정확**하다 — 과거
|
|
21
|
+
* `enterWith` 방식이 공유 동기 실행을 오염시켜 sibling 을 잘못 묶던 문제(스모크로 확인)를 구조적으로
|
|
22
|
+
* 제거한다. 이 seam 은 ADR-077 의 `_instrument` 에 추가된 **단 한 줄(run 위임)** 이다(ADR-114, 오너 결정).
|
|
23
|
+
*
|
|
24
|
+
* # 신규 의존성 (ADR-114 + ADR-126 보강)
|
|
25
|
+
* `@opentelemetry/api`, `@opentelemetry/sdk-trace-base`, `@opentelemetry/resources`,
|
|
26
|
+
* `@opentelemetry/semantic-conventions`, `@opentelemetry/exporter-trace-otlp-http`(OTLP exporter),
|
|
27
|
+
* `@opentelemetry/exporter-zipkin`(Zipkin exporter, ADR-126 보강 — 사용자 승인). audit 신규 0건.
|
|
28
|
+
*
|
|
29
|
+
* @module lib/mega-tracing
|
|
30
|
+
*/
|
|
31
|
+
import { trace, ROOT_CONTEXT, SpanKind, SpanStatusCode } from '@opentelemetry/api'
|
|
32
|
+
import {
|
|
33
|
+
BasicTracerProvider,
|
|
34
|
+
SimpleSpanProcessor,
|
|
35
|
+
BatchSpanProcessor,
|
|
36
|
+
ConsoleSpanExporter,
|
|
37
|
+
InMemorySpanExporter,
|
|
38
|
+
AlwaysOnSampler,
|
|
39
|
+
AlwaysOffSampler,
|
|
40
|
+
TraceIdRatioBasedSampler,
|
|
41
|
+
ParentBasedSampler,
|
|
42
|
+
} 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'
|
|
51
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
|
52
|
+
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
|
|
53
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
54
|
+
import { MegaConfigError } from '../errors/config-error.js'
|
|
55
|
+
|
|
56
|
+
/** tracer 이름 (instrumentation scope) — OTel 컨벤션상 패키지명 사용. */
|
|
57
|
+
const TRACER_NAME = 'mega-framework'
|
|
58
|
+
|
|
59
|
+
// 배선 코드(mega-app/ws-upgrade)가 @opentelemetry/api 를 직접 import 하지 않고 span kind 를 지정할 수
|
|
60
|
+
// 있게 re-export 한다(SERVER=HTTP/WS 수신, CLIENT=어댑터 호출, INTERNAL=사용자 span 기본).
|
|
61
|
+
export { SpanKind } from '@opentelemetry/api'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* **요청 단위 active OTel Context** 저장소 (ADR-126).
|
|
65
|
+
*
|
|
66
|
+
* HTTP/WS 루트 span 과 `ctx.tracer.span` 이 "지금 열려 있는 span" 을 비동기 흐름 전반에 전파하는 데 쓴다.
|
|
67
|
+
* 어댑터 자동 span(ADR-114)이 쓰는 per-adapter `MegaAdapter.#callScope` 와 **별개의 ALS** 다 — 어댑터
|
|
68
|
+
* 스코프가 없을 때(핸들러 최상위의 어댑터 호출 등) `handleStart` 가 이 저장소를 부모로 폴백해, 어댑터 span
|
|
69
|
+
* 이 HTTP/WS 루트 span 의 자식으로 정확히 중첩된다. `context-async-hooks` 패키지(신규 dep)를 도입하지
|
|
70
|
+
* 않고 우리 ALS 로 같은 효과를 낸다(ADR-114 의 "context manager 없이" 방침과 정합).
|
|
71
|
+
* @type {AsyncLocalStorage<import('@opentelemetry/api').Context>}
|
|
72
|
+
*/
|
|
73
|
+
const activeContextStore = new AsyncLocalStorage()
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 현재 활성 OTel Context — ALS 에 없으면 ROOT_CONTEXT(span 없음).
|
|
77
|
+
* @returns {import('@opentelemetry/api').Context}
|
|
78
|
+
*/
|
|
79
|
+
function getActiveContext() {
|
|
80
|
+
return activeContextStore.getStore() ?? ROOT_CONTEXT
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 비활성(옵트인 OFF) 시 사용자 코드에 넘길 **no-op span** — 메서드는 다 있지만 아무것도 기록 안 함.
|
|
85
|
+
* `ctx.tracer.span(name, (span) => span.setAttribute(...))` 가 OFF 에서도 깨지지 않게 한다.
|
|
86
|
+
* @type {any}
|
|
87
|
+
*/
|
|
88
|
+
const NOOP_SPAN = Object.freeze({
|
|
89
|
+
setAttribute: () => NOOP_SPAN,
|
|
90
|
+
setAttributes: () => NOOP_SPAN,
|
|
91
|
+
addEvent: () => NOOP_SPAN,
|
|
92
|
+
addLink: () => NOOP_SPAN,
|
|
93
|
+
addLinks: () => NOOP_SPAN,
|
|
94
|
+
recordException: () => {},
|
|
95
|
+
setStatus: () => NOOP_SPAN,
|
|
96
|
+
updateName: () => NOOP_SPAN,
|
|
97
|
+
end: () => {},
|
|
98
|
+
isRecording: () => false,
|
|
99
|
+
spanContext: () => ({ traceId: '', spanId: '', traceFlags: 0 }),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/** 비활성 시 `enterSpan` 이 돌려주는 no-op 핸들. */
|
|
103
|
+
const NOOP_HANDLE = Object.freeze({ span: NOOP_SPAN, end: () => {} })
|
|
104
|
+
|
|
105
|
+
/** 비활성 시 `enterHttpSpan` 이 돌려주는 no-op 핸들. */
|
|
106
|
+
const NOOP_HTTP_HANDLE = Object.freeze({ span: NOOP_SPAN, setError: () => {}, finish: () => {} })
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {object} TracingState
|
|
110
|
+
* @property {import('@opentelemetry/sdk-trace-base').BasicTracerProvider} provider
|
|
111
|
+
* @property {import('@opentelemetry/api').Tracer} tracer
|
|
112
|
+
* @property {import('@opentelemetry/sdk-trace-base').SpanExporter} exporter
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/** @type {TracingState | null} init 전이면 null = 비활성(no-op). */
|
|
116
|
+
let state = null
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 어댑터 → 구독 해제 함수. shutdown 시 일괄 해제. init/shutdown 사이클을 넘어 살아남는 모듈 싱글톤.
|
|
120
|
+
* @type {Map<import('../adapters/mega-adapter.js').MegaAdapter, () => void>}
|
|
121
|
+
*/
|
|
122
|
+
const subscriptions = new Map()
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 활성 여부 (Boolean — `is*`, ADR-036). `init` 후 `shutdown` 전이면 true.
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
export function isEnabled() {
|
|
129
|
+
return state !== null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* OTel 트레이싱 초기화. 옵트인 — 본 함수를 부르지 않으면 어떤 어댑터에도 리스너가 안 붙어 0 비용.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} opts
|
|
136
|
+
* @param {string} opts.serviceName - **필수**. `service.name` resource 속성.
|
|
137
|
+
* @param {string} [opts.endpoint] - OTLP HTTP traces endpoint (예: `http://localhost:4318/v1/traces`).
|
|
138
|
+
* 지정 + `exporter` 미지정 시 자동으로 `'otlp'` exporter 선택.
|
|
139
|
+
* @param {'always_on'|'always_off'|`traceidratio:${number}`|number} [opts.sampling='always_on'] -
|
|
140
|
+
* 샘플러. 비율은 `'traceidratio:0.1'` 또는 숫자 `0.1`(0~1). 비율 샘플러는 `ParentBased` 로 감싼다.
|
|
141
|
+
* @param {'console'|'otlp'|'zipkin'|'inmemory'|import('@opentelemetry/sdk-trace-base').SpanExporter} [opts.exporter] -
|
|
142
|
+
* exporter 종류 문자열 또는 인스턴스 직접 주입(테스트는 `InMemorySpanExporter` 인스턴스). 미지정 시
|
|
143
|
+
* `endpoint` 있으면 `'otlp'`, 없으면 `'console'`. `'zipkin'` 은 Zipkin v2 JSON(ADR-126 보강).
|
|
144
|
+
* @param {'simple'|'batch'} [opts.processor] - span processor. 미지정 시 otlp=`'batch'`, 그 외=`'simple'`.
|
|
145
|
+
* @param {string} [opts.version] - `service.version` resource 속성.
|
|
146
|
+
* @param {string} [opts.environment] - `deployment.environment.name` resource 속성.
|
|
147
|
+
* @param {Record<string, any>} [opts.attributes] - 추가 resource 속성(머지). 시크릿 금지.
|
|
148
|
+
* @returns {void}
|
|
149
|
+
* @throws {MegaConfigError} `tracing.already_initialized` / `tracing.service_name_required` / `tracing.invalid_sampling` / `tracing.invalid_exporter`.
|
|
150
|
+
*/
|
|
151
|
+
export function init(opts = /** @type {any} */ ({})) {
|
|
152
|
+
if (state !== null) {
|
|
153
|
+
throw new MegaConfigError(
|
|
154
|
+
'tracing.already_initialized',
|
|
155
|
+
'MegaTracing.init() called twice — call shutdown() first to re-initialize.',
|
|
156
|
+
{},
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
const serviceName = opts.serviceName
|
|
160
|
+
if (typeof serviceName !== 'string' || serviceName.length === 0) {
|
|
161
|
+
throw new MegaConfigError(
|
|
162
|
+
'tracing.service_name_required',
|
|
163
|
+
'MegaTracing.init({ serviceName }) is required (non-empty string).',
|
|
164
|
+
{},
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sampler = buildSampler(opts.sampling)
|
|
169
|
+
const { exporter, processorKind } = buildExporter(opts)
|
|
170
|
+
const SpanProcessor = processorKind === 'batch' ? BatchSpanProcessor : SimpleSpanProcessor
|
|
171
|
+
|
|
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
|
+
})
|
|
178
|
+
|
|
179
|
+
const provider = new BasicTracerProvider({
|
|
180
|
+
resource,
|
|
181
|
+
sampler,
|
|
182
|
+
spanProcessors: [new SpanProcessor(exporter)],
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
state = {
|
|
186
|
+
provider,
|
|
187
|
+
tracer: provider.getTracer(TRACER_NAME),
|
|
188
|
+
exporter,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* `MEGA_OTEL_*` 환경변수로 옵트인 초기화 (12-factor). `MEGA_OTEL_ENABLED!=='true'` 면 **no-op**
|
|
194
|
+
* (init 미호출 → 0 비용). 활성 시 `MEGA_OTEL_SERVICE_NAME` 필수.
|
|
195
|
+
*
|
|
196
|
+
* 매핑:
|
|
197
|
+
* - `MEGA_OTEL_ENABLED` (true|false, 디폴트 false)
|
|
198
|
+
* - `MEGA_OTEL_SERVICE_NAME` → serviceName (필수)
|
|
199
|
+
* - `MEGA_OTEL_ENDPOINT` → endpoint (OTLP)
|
|
200
|
+
* - `MEGA_OTEL_SAMPLING_RATIO` → sampling (숫자 비율; `always_on`/`always_off` 문자열도 허용)
|
|
201
|
+
* - `MEGA_OTEL_EXPORTER` → exporter (otlp|console|inmemory)
|
|
202
|
+
* - `MEGA_OTEL_VERSION` / `MEGA_OTEL_ENVIRONMENT` → resource 속성
|
|
203
|
+
*
|
|
204
|
+
* @param {Record<string, string|undefined>} [env=process.env]
|
|
205
|
+
* @returns {boolean} 활성화돼 init 했으면 true, 옵트인 OFF 면 false.
|
|
206
|
+
* @throws {MegaConfigError} 활성인데 serviceName 누락 시 `tracing.service_name_required`.
|
|
207
|
+
*/
|
|
208
|
+
export function fromEnv(env = process.env) {
|
|
209
|
+
if (env.MEGA_OTEL_ENABLED !== 'true') return false
|
|
210
|
+
/** @type {any} */
|
|
211
|
+
const opts = { serviceName: env.MEGA_OTEL_SERVICE_NAME }
|
|
212
|
+
if (env.MEGA_OTEL_ENDPOINT) opts.endpoint = env.MEGA_OTEL_ENDPOINT
|
|
213
|
+
if (env.MEGA_OTEL_EXPORTER) opts.exporter = env.MEGA_OTEL_EXPORTER
|
|
214
|
+
if (env.MEGA_OTEL_VERSION) opts.version = env.MEGA_OTEL_VERSION
|
|
215
|
+
if (env.MEGA_OTEL_ENVIRONMENT) opts.environment = env.MEGA_OTEL_ENVIRONMENT
|
|
216
|
+
const ratio = env.MEGA_OTEL_SAMPLING_RATIO
|
|
217
|
+
if (ratio !== undefined && ratio !== '') {
|
|
218
|
+
opts.sampling = ratio === 'always_on' || ratio === 'always_off' ? ratio : Number(ratio)
|
|
219
|
+
}
|
|
220
|
+
init(opts)
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 단일 어댑터의 `onCallStart`/`onCallEnd` 를 구독해 span 으로 변환한다. init 전이면 no-op(리스너 미부착).
|
|
226
|
+
* 이미 구독된 어댑터를 다시 부르면 기존 구독을 유지(중복 부착 방지).
|
|
227
|
+
*
|
|
228
|
+
* @param {import('../adapters/mega-adapter.js').MegaAdapter} adapter
|
|
229
|
+
* @param {{ domain?: string, driver?: string, key?: string }} [meta] - span 속성 보강용 메타.
|
|
230
|
+
* @returns {() => void} 구독 해제 함수.
|
|
231
|
+
*/
|
|
232
|
+
export function subscribe(adapter, meta = {}) {
|
|
233
|
+
if (state === null) return () => {} // 옵트인 OFF — 리스너 미부착.
|
|
234
|
+
const existing = subscriptions.get(adapter)
|
|
235
|
+
if (existing) return existing
|
|
236
|
+
|
|
237
|
+
/** @type {{ adapterName: string, domain: string|null, driver: string|null, key: string|null }} */
|
|
238
|
+
const m = {
|
|
239
|
+
adapterName: adapter?.constructor?.name ?? 'MegaAdapter',
|
|
240
|
+
domain: meta.domain ?? null,
|
|
241
|
+
driver: meta.driver ?? null,
|
|
242
|
+
key: meta.key ?? null,
|
|
243
|
+
}
|
|
244
|
+
/** @type {import('../adapters/mega-adapter.js').HookListener} */
|
|
245
|
+
const onStart = (callName, attrs) => {
|
|
246
|
+
try {
|
|
247
|
+
// 부모 = 진행 중 바깥 호출의 스코프 토큰(베이스 `run()` 이 설정). 없으면 루트.
|
|
248
|
+
return handleStart(m, adapter._currentCallScope, callName, attrs)
|
|
249
|
+
} catch (err) {
|
|
250
|
+
// 트레이싱 버그가 도메인 호출을 깨면 안 됨(M1 정합) — 표면화 후 무시.
|
|
251
|
+
console.warn('[MegaTracing] onCallStart handler error (ignored):', err)
|
|
252
|
+
return undefined
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** @type {import('../adapters/mega-adapter.js').HookListener} */
|
|
256
|
+
const onEnd = (callName, attrs, err, scope) => {
|
|
257
|
+
try {
|
|
258
|
+
handleEnd(scope, attrs, err)
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn('[MegaTracing] onCallEnd handler error (ignored):', e)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const unStart = adapter.addHookListener('onCallStart', onStart)
|
|
264
|
+
const unEnd = adapter.addHookListener('onCallEnd', onEnd)
|
|
265
|
+
const unsubscribe = () => {
|
|
266
|
+
unStart()
|
|
267
|
+
unEnd()
|
|
268
|
+
subscriptions.delete(adapter)
|
|
269
|
+
}
|
|
270
|
+
subscriptions.set(adapter, unsubscribe)
|
|
271
|
+
return unsubscribe
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 부팅된 전역 어댑터 매니저의 **모든 공유 어댑터**에 hook 리스너를 일괄 구독한다(ADR-102 글로벌 공유).
|
|
276
|
+
* `connectAll` 이후 호출 권장(연결 안 된 어댑터도 구독은 안전 — 첫 호출 때 span 발화).
|
|
277
|
+
*
|
|
278
|
+
* @param {{ entries: () => Array<{ domain: string, key: string, driver: string, adapter: import('../adapters/mega-adapter.js').MegaAdapter }> }} manager -
|
|
279
|
+
* `MegaAdapterManager` (entries() 노출). 테스트 더블도 같은 형태면 됨.
|
|
280
|
+
* @returns {number} 구독한 어댑터 수.
|
|
281
|
+
*/
|
|
282
|
+
export function attachToManager(manager) {
|
|
283
|
+
if (state === null) return 0 // 옵트인 OFF.
|
|
284
|
+
let count = 0
|
|
285
|
+
for (const e of manager.entries()) {
|
|
286
|
+
subscribe(e.adapter, { domain: e.domain, driver: e.driver, key: e.key })
|
|
287
|
+
count += 1
|
|
288
|
+
}
|
|
289
|
+
return count
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 트레이싱 종료 — 모든 구독 해제 + 남은 span flush + provider 종료. init 전이면 no-op.
|
|
294
|
+
* @returns {Promise<void>}
|
|
295
|
+
*/
|
|
296
|
+
export async function shutdown() {
|
|
297
|
+
for (const unsubscribe of [...subscriptions.values()]) unsubscribe()
|
|
298
|
+
subscriptions.clear()
|
|
299
|
+
const s = state
|
|
300
|
+
state = null
|
|
301
|
+
// 활성 컨텍스트 ALS 비활성화 — 남은 enterWith 컨텍스트를 모두 비운다(다음 run()/enterWith 가 재활성화).
|
|
302
|
+
activeContextStore.disable()
|
|
303
|
+
if (s !== null) {
|
|
304
|
+
await s.provider.forceFlush()
|
|
305
|
+
await s.provider.shutdown()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 현재 exporter 반환(테스트·디버그용). `InMemorySpanExporter` 면 `.getFinishedSpans()` 로 span 검사.
|
|
311
|
+
* @returns {import('@opentelemetry/sdk-trace-base').SpanExporter | null}
|
|
312
|
+
*/
|
|
313
|
+
export function getExporter() {
|
|
314
|
+
return state?.exporter ?? null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 강제 flush — `BatchSpanProcessor` 사용 시 테스트에서 span 발화를 기다릴 때.
|
|
319
|
+
* @returns {Promise<void>}
|
|
320
|
+
*/
|
|
321
|
+
export async function forceFlush() {
|
|
322
|
+
if (state !== null) await state.provider.forceFlush()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
326
|
+
// 사용자/요청 span API (ADR-126) — HTTP/WS 루트 span · ctx.tracer.span · 로그 trace_id
|
|
327
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* **사용자 직접 span** — `name` span 을 현재 활성 span(없으면 루트)의 자식으로 열고 `fn(span)` 을 그
|
|
331
|
+
* span 컨텍스트 안에서 실행한다(`run()` 격리). `fn` 이 Promise 면 resolve/reject 까지 기다려 span 을
|
|
332
|
+
* 닫는다. 옵트인 OFF 면 0 비용 — span 없이 `fn(NOOP_SPAN)` 만 호출한다. `ctx.tracer.span` 의 구현부.
|
|
333
|
+
*
|
|
334
|
+
* @template T
|
|
335
|
+
* @param {string} name - span 이름(예: `'charge.process'`).
|
|
336
|
+
* @param {(span: import('@opentelemetry/api').Span) => T} fn - span 안에서 실행할 함수(span 인자 받음).
|
|
337
|
+
* @param {{ attributes?: Record<string, any>, kind?: import('@opentelemetry/api').SpanKind }} [opts]
|
|
338
|
+
* @returns {T} `fn` 의 반환값(그대로 — 동기/Promise 모두).
|
|
339
|
+
* @throws {*} `fn` 이 throw/reject 하면 span 에 기록(ERROR)하고 그대로 재전파(silent 금지).
|
|
340
|
+
*/
|
|
341
|
+
export function span(name, fn, opts = {}) {
|
|
342
|
+
if (state === null) return fn(NOOP_SPAN) // 옵트인 OFF — 0 비용.
|
|
343
|
+
const parentCtx = getActiveContext()
|
|
344
|
+
const sp = state.tracer.startSpan(
|
|
345
|
+
name,
|
|
346
|
+
{ kind: opts.kind, attributes: filterAttributes(opts.attributes) },
|
|
347
|
+
parentCtx,
|
|
348
|
+
)
|
|
349
|
+
const childCtx = trace.setSpan(parentCtx, sp)
|
|
350
|
+
// run() 격리 — fn 내부의 중첩 span/어댑터 호출이 sp 를 부모로 읽는다(요청 ALS).
|
|
351
|
+
return activeContextStore.run(childCtx, () => {
|
|
352
|
+
/** @type {any} */
|
|
353
|
+
let result
|
|
354
|
+
try {
|
|
355
|
+
result = fn(sp)
|
|
356
|
+
} catch (err) {
|
|
357
|
+
endSpanWith(sp, err)
|
|
358
|
+
throw err
|
|
359
|
+
}
|
|
360
|
+
if (result && typeof result.then === 'function') {
|
|
361
|
+
return /** @type {any} */ (
|
|
362
|
+
result.then(
|
|
363
|
+
(/** @type {any} */ v) => {
|
|
364
|
+
endSpanWith(sp, undefined)
|
|
365
|
+
return v
|
|
366
|
+
},
|
|
367
|
+
(/** @type {any} */ err) => {
|
|
368
|
+
endSpanWith(sp, err)
|
|
369
|
+
throw err
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
endSpanWith(sp, undefined)
|
|
375
|
+
return result
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* **시작/종료 분리형 span** — HTTP `onRequest`/`onResponse` 처럼 시작과 종료가 다른 콜백인 경우에 쓴다.
|
|
381
|
+
* span 을 열고 `enterWith` 로 현재 비동기 흐름의 활성 컨텍스트를 이 span 으로 만든 뒤, `end(err)` 를
|
|
382
|
+
* 가진 핸들을 돌려준다(핸들은 보통 `req` 에 보관했다가 응답 시 닫는다). 옵트인 OFF 면 no-op 핸들.
|
|
383
|
+
*
|
|
384
|
+
* ⚠️ `enterWith` 는 **요청별로 격리된 비동기 흐름**에서만 호출해야 한다(HTTP 요청·WS 메시지 콜백은 각각
|
|
385
|
+
* 독립 async context 라 안전 — 실측 확인). 공유 동기 스코프에서 부르면 sibling 오염(ADR-114) 위험.
|
|
386
|
+
*
|
|
387
|
+
* @param {string} name
|
|
388
|
+
* @param {{ attributes?: Record<string, any>, kind?: import('@opentelemetry/api').SpanKind, parent?: import('@opentelemetry/api').Context }} [opts]
|
|
389
|
+
* @returns {{ span: import('@opentelemetry/api').Span, end: (err?: unknown, endAttributes?: Record<string, any>) => void }}
|
|
390
|
+
*/
|
|
391
|
+
export function enterSpan(name, opts = {}) {
|
|
392
|
+
if (state === null) return /** @type {any} */ (NOOP_HANDLE)
|
|
393
|
+
const parentCtx = opts.parent ?? getActiveContext()
|
|
394
|
+
const sp = state.tracer.startSpan(
|
|
395
|
+
name,
|
|
396
|
+
{ kind: opts.kind, attributes: filterAttributes(opts.attributes) },
|
|
397
|
+
parentCtx,
|
|
398
|
+
)
|
|
399
|
+
activeContextStore.enterWith(trace.setSpan(parentCtx, sp))
|
|
400
|
+
return {
|
|
401
|
+
span: sp,
|
|
402
|
+
end(err, endAttributes) {
|
|
403
|
+
if (endAttributes) sp.setAttributes(filterAttributes(endAttributes))
|
|
404
|
+
endSpanWith(sp, err)
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* **HTTP 루트 span** 전용 헬퍼 — `onRequest` 에서 열고(`enterWith` 로 활성화), `onError` 에서 예외를
|
|
411
|
+
* 기록(`setError`), `onResponse` 에서 상태코드와 함께 닫는다(`finish`). HTTP 시맨틱(5xx=ERROR)을 한곳에
|
|
412
|
+
* 모아 배선 코드(mega-app)가 OTel enum 을 안 만지게 한다. 옵트인 OFF 면 no-op 핸들.
|
|
413
|
+
*
|
|
414
|
+
* @param {{ method: string, route: string, path: string, host?: string, app: string }} info
|
|
415
|
+
* @returns {{ span: import('@opentelemetry/api').Span, setError: (err: unknown) => void, finish: (statusCode: number) => void }}
|
|
416
|
+
*/
|
|
417
|
+
export function enterHttpSpan({ method, route, path, host, app }) {
|
|
418
|
+
if (state === null) return /** @type {any} */ (NOOP_HTTP_HANDLE)
|
|
419
|
+
const handle = enterSpan(`http.${method} ${route}`, {
|
|
420
|
+
kind: SpanKind.SERVER,
|
|
421
|
+
attributes: {
|
|
422
|
+
'http.request.method': method,
|
|
423
|
+
'http.route': route,
|
|
424
|
+
'url.path': path,
|
|
425
|
+
...(host ? { 'server.address': host } : {}),
|
|
426
|
+
'mega.app': app,
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
let errored = false
|
|
430
|
+
return {
|
|
431
|
+
span: handle.span,
|
|
432
|
+
setError(err) {
|
|
433
|
+
errored = true
|
|
434
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
435
|
+
handle.span.recordException(e)
|
|
436
|
+
handle.span.setStatus({ code: SpanStatusCode.ERROR, message: e.message })
|
|
437
|
+
},
|
|
438
|
+
finish(statusCode) {
|
|
439
|
+
handle.span.setAttribute('http.response.status_code', statusCode)
|
|
440
|
+
// OTel: SERVER span 은 5xx 만 ERROR. 4xx 는 클라이언트 잘못이라 UNSET 유지(이미 errored 면 보존).
|
|
441
|
+
if (!errored && Number(statusCode) >= 500) {
|
|
442
|
+
handle.span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${statusCode}` })
|
|
443
|
+
}
|
|
444
|
+
handle.span.end()
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 현재 활성 span 의 trace/span id (로그 상관관계용). 활성 span 없거나 옵트인 OFF 면 null.
|
|
451
|
+
* @returns {{ traceId: string, spanId: string } | null}
|
|
452
|
+
*/
|
|
453
|
+
export function currentTraceIds() {
|
|
454
|
+
if (state === null) return null
|
|
455
|
+
const sp = trace.getSpan(getActiveContext())
|
|
456
|
+
if (!sp) return null
|
|
457
|
+
const sc = sp.spanContext()
|
|
458
|
+
if (!sc || typeof sc.traceId !== 'string' || sc.traceId === '' || sc.traceId === '0'.repeat(32)) return null
|
|
459
|
+
return { traceId: sc.traceId, spanId: sc.spanId }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* **pino mixin** — 활성 span 이 있으면 모든 로그 라인에 `{ trace_id, span_id }` 를 주입한다(ADR-116 (d)).
|
|
464
|
+
* 로거가 켜진 앱에서 `logger: { mixin: MegaTracing.logMixin }` 로 배선하면 요청 로그가 trace 와 상관된다.
|
|
465
|
+
* 활성 span 없으면 빈 객체(주입 없음). 옵트인 OFF 면 항상 빈 객체 → 0 비용.
|
|
466
|
+
* @returns {{ trace_id?: string, span_id?: string }}
|
|
467
|
+
*/
|
|
468
|
+
export function logMixin() {
|
|
469
|
+
const ids = currentTraceIds()
|
|
470
|
+
return ids ? { trace_id: ids.traceId, span_id: ids.spanId } : {}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* `ctx.tracer` 로 노출하는 트레이서 표면(ADR-077/116 canonical `ctx.tracer.span`). 모듈 싱글톤 — 활성
|
|
475
|
+
* 컨텍스트는 ALS 가 요청별로 들고 있으므로 객체 1개를 모든 요청이 공유해도 안전(getter 가 ALS 를 읽음).
|
|
476
|
+
*/
|
|
477
|
+
export const tracer = Object.freeze({
|
|
478
|
+
span,
|
|
479
|
+
/** @returns {import('@opentelemetry/api').Span | undefined} 현재 활성 span(없으면 undefined — 03-api-spec §10). */
|
|
480
|
+
activeSpan() {
|
|
481
|
+
if (state === null) return undefined
|
|
482
|
+
return trace.getSpan(getActiveContext())
|
|
483
|
+
},
|
|
484
|
+
/** @returns {string|null} 현재 활성 trace id(없으면 null). */
|
|
485
|
+
get traceId() {
|
|
486
|
+
return currentTraceIds()?.traceId ?? null
|
|
487
|
+
},
|
|
488
|
+
/** @returns {string|null} 현재 활성 span id(없으면 null). */
|
|
489
|
+
get spanId() {
|
|
490
|
+
return currentTraceIds()?.spanId ?? null
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
495
|
+
// 내부 — span 시작/종료 핸들러 (hook 리스너가 호출)
|
|
496
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* span 을 결과(성공/에러)에 맞게 닫는다 — 공통 종료 로직(상태·예외 기록).
|
|
500
|
+
* @param {import('@opentelemetry/api').Span} sp
|
|
501
|
+
* @param {unknown} err - 있으면 ERROR + recordException, 없으면 OK.
|
|
502
|
+
* @returns {void}
|
|
503
|
+
*/
|
|
504
|
+
function endSpanWith(sp, err) {
|
|
505
|
+
if (err) {
|
|
506
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
507
|
+
sp.recordException(e)
|
|
508
|
+
sp.setStatus({ code: SpanStatusCode.ERROR, message: e.message })
|
|
509
|
+
} else {
|
|
510
|
+
sp.setStatus({ code: SpanStatusCode.OK })
|
|
511
|
+
}
|
|
512
|
+
sp.end()
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* attrs 에서 OTel 허용 primitive 만 골라 **새 객체**로 반환한다(span 시작 속성용).
|
|
517
|
+
* @param {Record<string, any>} [attrs]
|
|
518
|
+
* @returns {Record<string, any>}
|
|
519
|
+
*/
|
|
520
|
+
function filterAttributes(attrs) {
|
|
521
|
+
/** @type {Record<string, any>} */
|
|
522
|
+
const out = {}
|
|
523
|
+
mergePrimitiveAttrs(out, attrs)
|
|
524
|
+
return out
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* `onCallStart` → span 시작. 부모 스코프 토큰(있으면)을 parent context 로 삼아 span 을 만들고,
|
|
529
|
+
* 자식이 부모로 읽을 **스코프 토큰**(span 을 담은 OTel context)을 반환한다. 베이스 `_instrument` 가
|
|
530
|
+
* 이 토큰으로 도메인 fn 을 `run()` 안에서 실행한다.
|
|
531
|
+
*
|
|
532
|
+
* @param {{ adapterName: string, domain: string|null, driver: string|null, key: string|null }} meta
|
|
533
|
+
* @param {import('@opentelemetry/api').Context | undefined} parentScope - `adapter._currentCallScope`.
|
|
534
|
+
* @param {string} callName
|
|
535
|
+
* @param {object} [attrs]
|
|
536
|
+
* @returns {import('@opentelemetry/api').Context | undefined} 스코프 토큰(span 담은 context) 또는 undefined(비활성).
|
|
537
|
+
*/
|
|
538
|
+
function handleStart(meta, parentScope, callName, attrs) {
|
|
539
|
+
if (state === null) return undefined
|
|
540
|
+
// 부모 우선순위: (1) 진행 중 바깥 어댑터 호출의 스코프(`run()` seam) → (2) HTTP/WS/ctx.tracer 가 연 활성
|
|
541
|
+
// span(요청 ALS) → (3) 루트. (2) 폴백 덕에 핸들러 최상위의 어댑터 호출이 HTTP 루트 span 의 자식이 된다.
|
|
542
|
+
const parentCtx = parentScope ?? getActiveContext()
|
|
543
|
+
const name = meta.driver ? `${meta.driver}.${callName}` : callName
|
|
544
|
+
const span = state.tracer.startSpan(
|
|
545
|
+
name,
|
|
546
|
+
{ kind: SpanKind.CLIENT, attributes: buildStartAttributes(meta, callName, attrs) },
|
|
547
|
+
parentCtx,
|
|
548
|
+
)
|
|
549
|
+
return trace.setSpan(parentCtx, span)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* `onCallEnd` → 스코프 토큰에서 span 을 꺼내 종료한다. 토큰이 없으면(비활성 호출) 무시.
|
|
554
|
+
* `state`(활성) 여부와 무관하게 토큰의 span 을 닫아 누수를 막는다(shutdown 직후 in-flight 케이스).
|
|
555
|
+
*
|
|
556
|
+
* @param {import('@opentelemetry/api').Context | undefined} scope - 짝 `onCallStart` 의 반환 토큰.
|
|
557
|
+
* @param {object} [attrs] - 결과 속성(`{ ...startAttrs, latencyMs }`).
|
|
558
|
+
* @param {Error} [err] - 있으면 status=ERROR + recordException.
|
|
559
|
+
* @returns {void}
|
|
560
|
+
*/
|
|
561
|
+
function handleEnd(scope, attrs, err) {
|
|
562
|
+
const span = scope ? trace.getSpan(scope) : undefined
|
|
563
|
+
if (!span) return // 비활성 호출(토큰 없음) 또는 불균형 — 방어적 무시.
|
|
564
|
+
applyEndAttributes(span, attrs)
|
|
565
|
+
if (err) {
|
|
566
|
+
span.recordException(err)
|
|
567
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) })
|
|
568
|
+
} else {
|
|
569
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
570
|
+
}
|
|
571
|
+
span.end()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* 시작 span 속성 구성 — 어댑터 메타(`mega.*`) + 도메인별 시맨틱(`db.system.name`) + 사용자 attrs.
|
|
576
|
+
* @param {{ adapterName: string, domain: string|null, driver: string|null, key: string|null }} meta
|
|
577
|
+
* @param {string} callName
|
|
578
|
+
* @param {object} [attrs]
|
|
579
|
+
* @returns {Record<string, any>}
|
|
580
|
+
*/
|
|
581
|
+
function buildStartAttributes(meta, callName, attrs) {
|
|
582
|
+
/** @type {Record<string, any>} */
|
|
583
|
+
const out = { 'mega.adapter': meta.adapterName, 'mega.call': callName }
|
|
584
|
+
if (meta.domain) out['mega.domain'] = meta.domain
|
|
585
|
+
if (meta.driver) out['mega.driver'] = meta.driver
|
|
586
|
+
if (meta.key) out['mega.key'] = meta.key
|
|
587
|
+
// DB 도메인은 OTel 시맨틱 컨벤션 `db.system.name` 으로도 노출(백엔드 자동 인식).
|
|
588
|
+
if (meta.domain === 'db' && meta.driver) out[ATTR_DB_SYSTEM_NAME] = meta.driver
|
|
589
|
+
mergePrimitiveAttrs(out, attrs)
|
|
590
|
+
// 어댑터는 OTel 무지 — `statement`(파라미터화된 SQL) 키로 넘기면 여기서 표준 `db.query.text` 로
|
|
591
|
+
// 매핑한다(ADR-138). DB 도메인 한정. 바인딩 값은 어댑터가 애초에 안 싣는다(시크릿·카디널리티 안전).
|
|
592
|
+
if (meta.domain === 'db' && typeof out.statement === 'string') {
|
|
593
|
+
out[ATTR_DB_QUERY_TEXT] = out.statement
|
|
594
|
+
delete out.statement
|
|
595
|
+
}
|
|
596
|
+
return out
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 종료 시 결과 속성 반영 — latency 를 `mega.latency_ms` 로, 그 외 신규 primitive attr 머지.
|
|
601
|
+
* @param {import('@opentelemetry/api').Span} span
|
|
602
|
+
* @param {object} [attrs]
|
|
603
|
+
* @returns {void}
|
|
604
|
+
*/
|
|
605
|
+
function applyEndAttributes(span, attrs) {
|
|
606
|
+
if (!attrs || typeof attrs !== 'object') return
|
|
607
|
+
const a = /** @type {Record<string, any>} */ (attrs)
|
|
608
|
+
if (typeof a.latencyMs === 'number') span.setAttribute('mega.latency_ms', a.latencyMs)
|
|
609
|
+
/** @type {Record<string, any>} */
|
|
610
|
+
const rest = {}
|
|
611
|
+
mergePrimitiveAttrs(rest, attrs)
|
|
612
|
+
delete rest.latencyMs // mega.latency_ms 로 이미 반영.
|
|
613
|
+
// `statement` 는 시작 span 에서 이미 `db.query.text` 로 매핑됐다(ADR-138). 종료 attr 은 시작 attr 을
|
|
614
|
+
// 다시 싣고 오므로, 여기서 원키를 지우지 않으면 매핑 전 `statement` 가 span 에 되살아난다.
|
|
615
|
+
delete rest.statement
|
|
616
|
+
if (Object.keys(rest).length > 0) span.setAttributes(rest)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* attrs 중 OTel 이 허용하는 primitive(문자열/숫자/불리언, 또는 그 배열)만 골라 머지한다.
|
|
621
|
+
* `undefined`/`null`/객체/함수는 건너뛴다(예: `withTransaction` 의 `{ table: undefined }`).
|
|
622
|
+
* @param {Record<string, any>} target
|
|
623
|
+
* @param {object} [attrs]
|
|
624
|
+
* @returns {void}
|
|
625
|
+
*/
|
|
626
|
+
function mergePrimitiveAttrs(target, attrs) {
|
|
627
|
+
if (!attrs || typeof attrs !== 'object') return
|
|
628
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
629
|
+
if (v === undefined || v === null) continue
|
|
630
|
+
const t = typeof v
|
|
631
|
+
if (t === 'string' || t === 'number' || t === 'boolean') {
|
|
632
|
+
target[k] = v
|
|
633
|
+
} else if (Array.isArray(v) && v.every((x) => ['string', 'number', 'boolean'].includes(typeof x))) {
|
|
634
|
+
target[k] = v
|
|
635
|
+
}
|
|
636
|
+
// 그 외(객체/함수/심볼)는 스킵 — OTel attribute 값으로 부적합 + 의도치 않은 페이로드 노출 방지.
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* 샘플링 옵션 → OTel Sampler. 비율은 `ParentBasedSampler(root=ratio)` 로 감싸 자식이 부모 결정을 따른다.
|
|
642
|
+
* @param {string|number|undefined} sampling
|
|
643
|
+
* @returns {import('@opentelemetry/sdk-trace-base').Sampler}
|
|
644
|
+
*/
|
|
645
|
+
function buildSampler(sampling) {
|
|
646
|
+
if (sampling === undefined || sampling === null || sampling === 'always_on') return new AlwaysOnSampler()
|
|
647
|
+
if (sampling === 'always_off') return new AlwaysOffSampler()
|
|
648
|
+
let ratio
|
|
649
|
+
if (typeof sampling === 'number') ratio = sampling
|
|
650
|
+
else if (typeof sampling === 'string' && sampling.startsWith('traceidratio:')) {
|
|
651
|
+
ratio = Number(sampling.slice('traceidratio:'.length))
|
|
652
|
+
} else {
|
|
653
|
+
throw new MegaConfigError(
|
|
654
|
+
'tracing.invalid_sampling',
|
|
655
|
+
`sampling must be 'always_on' | 'always_off' | 'traceidratio:<0..1>' | <number 0..1> (got '${sampling}').`,
|
|
656
|
+
{ details: { sampling } },
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
|
|
660
|
+
throw new MegaConfigError(
|
|
661
|
+
'tracing.invalid_sampling',
|
|
662
|
+
`sampling ratio must be a number in [0, 1] (got ${ratio}).`,
|
|
663
|
+
{ details: { sampling, ratio } },
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(ratio) })
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* exporter 옵션 → { exporter, processorKind }. 문자열 종류 또는 인스턴스 직접 주입을 받는다.
|
|
671
|
+
* @param {{ exporter?: any, endpoint?: string, processor?: 'simple'|'batch' }} opts
|
|
672
|
+
* @returns {{ exporter: import('@opentelemetry/sdk-trace-base').SpanExporter, processorKind: 'simple'|'batch' }}
|
|
673
|
+
*/
|
|
674
|
+
function buildExporter(opts) {
|
|
675
|
+
const explicitProcessor = opts.processor
|
|
676
|
+
// 인스턴스 직접 주입(테스트의 InMemorySpanExporter 등) — `export` 함수를 가진 객체로 판별.
|
|
677
|
+
if (opts.exporter && typeof opts.exporter === 'object' && typeof opts.exporter.export === 'function') {
|
|
678
|
+
return { exporter: opts.exporter, processorKind: explicitProcessor ?? 'simple' }
|
|
679
|
+
}
|
|
680
|
+
const kind = typeof opts.exporter === 'string' ? opts.exporter : opts.endpoint ? 'otlp' : 'console'
|
|
681
|
+
switch (kind) {
|
|
682
|
+
case 'console':
|
|
683
|
+
return { exporter: new ConsoleSpanExporter(), processorKind: explicitProcessor ?? 'simple' }
|
|
684
|
+
case 'inmemory':
|
|
685
|
+
return { exporter: new InMemorySpanExporter(), processorKind: explicitProcessor ?? 'simple' }
|
|
686
|
+
case 'otlp':
|
|
687
|
+
return {
|
|
688
|
+
exporter: new OTLPTraceExporter(opts.endpoint ? { url: opts.endpoint } : {}),
|
|
689
|
+
processorKind: explicitProcessor ?? 'batch',
|
|
690
|
+
}
|
|
691
|
+
case 'zipkin':
|
|
692
|
+
// Zipkin v2 JSON span 포맷. endpoint 미지정 시 ZipkinExporter 디폴트(http://localhost:9411/api/v2/spans).
|
|
693
|
+
return {
|
|
694
|
+
exporter: new ZipkinExporter(opts.endpoint ? { url: opts.endpoint } : {}),
|
|
695
|
+
processorKind: explicitProcessor ?? 'batch',
|
|
696
|
+
}
|
|
697
|
+
default:
|
|
698
|
+
throw new MegaConfigError(
|
|
699
|
+
'tracing.invalid_exporter',
|
|
700
|
+
`exporter must be 'console' | 'otlp' | 'zipkin' | 'inmemory' | <SpanExporter instance> (got '${kind}').`,
|
|
701
|
+
{ details: { exporter: kind } },
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* 테스트 격리용 reset — 동기 강제 정리(provider flush 없이 상태만 비움). 비동기 정상 종료는 `shutdown`.
|
|
708
|
+
* @returns {void}
|
|
709
|
+
*/
|
|
710
|
+
export function _reset() {
|
|
711
|
+
for (const unsubscribe of [...subscriptions.values()]) unsubscribe()
|
|
712
|
+
subscriptions.clear()
|
|
713
|
+
activeContextStore.disable()
|
|
714
|
+
state = null
|
|
715
|
+
}
|