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,80 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
<%
|
|
3
|
+
function fmt(d) { return new Date(d).toLocaleString('ko-KR', { hour12: false, timeZone: 'Asia/Seoul' }) }
|
|
4
|
+
function levelBadge(l) { return l === 'error' ? 'text-bg-danger' : l === 'warn' ? 'text-bg-warning' : l === 'debug' ? 'text-bg-secondary' : 'text-bg-info' }
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<div class="mb-4">
|
|
8
|
+
<h1 class="h3 mb-1"><%= t('logs_title', '구조적 로깅') %></h1>
|
|
9
|
+
<p class="text-body-secondary small mb-0"><%= t('logs_subtitle', 'pino 로그를 emit 하고 trace_id·민감필드 마스킹을 시연합니다.') %></p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if (typeof notice !== 'undefined' && notice) { %>
|
|
13
|
+
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
14
|
+
<%= notice %>
|
|
15
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
|
|
16
|
+
</div>
|
|
17
|
+
<% } %>
|
|
18
|
+
|
|
19
|
+
<div class="row g-3">
|
|
20
|
+
<div class="col-lg-5">
|
|
21
|
+
<div class="card h-100">
|
|
22
|
+
<div class="card-body">
|
|
23
|
+
<h2 class="h5 card-title"><%= t('logs_emit_title', '로그 emit') %></h2>
|
|
24
|
+
<p class="card-text text-body-secondary small"><%= t('logs_emit_desc', '선택한 레벨로 로그 1건을 실제 logger 로 내보냅니다.') %></p>
|
|
25
|
+
<form method="post" action="/demo/logs/emit">
|
|
26
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
27
|
+
<div class="mb-2">
|
|
28
|
+
<label class="form-label small mb-1" for="level"><%= t('logs_level', '레벨') %></label>
|
|
29
|
+
<select class="form-select form-select-sm" id="level" name="level">
|
|
30
|
+
<% snap.levels.forEach(function (l) { %>
|
|
31
|
+
<option value="<%= l %>" <%= l === 'info' ? 'selected' : '' %>><%= l %></option>
|
|
32
|
+
<% }) %>
|
|
33
|
+
</select>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="mb-3">
|
|
36
|
+
<label class="form-label small mb-1" for="message"><%= t('logs_message', '메시지') %></label>
|
|
37
|
+
<input type="text" class="form-control form-control-sm" id="message" name="message" placeholder="<%= t('logs_message_ph', '예: 데모 로그 한 줄') %>" />
|
|
38
|
+
</div>
|
|
39
|
+
<button type="submit" class="btn btn-primary btn-sm"><%= t('logs_emit_btn', 'emit') %></button>
|
|
40
|
+
</form>
|
|
41
|
+
<div class="alert alert-info small mt-3 mb-0"><%= t('logs_redact_hint', '로그 payload 의 token/password/secret 은 console 출력에서 [Redacted] 로 마스킹되고, 활성 trace_id 가 자동 첨부됩니다 (ADR-141). 서버 콘솔에서 확인하세요.') %></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="col-lg-7">
|
|
47
|
+
<div class="card h-100">
|
|
48
|
+
<div class="card-body">
|
|
49
|
+
<h2 class="h5 card-title"><%= t('logs_recent_title', '최근 emit') %></h2>
|
|
50
|
+
<p class="card-text text-body-secondary small"><%= t('logs_recent_desc', 'emit 한 로그의 안전한 메타데이터입니다 (시크릿 제외).') %></p>
|
|
51
|
+
<% if (snap.recent.length === 0) { %>
|
|
52
|
+
<p class="text-body-secondary small mb-0"><%= t('logs_recent_empty', '아직 emit 한 로그가 없습니다.') %></p>
|
|
53
|
+
<% } else { %>
|
|
54
|
+
<div class="table-responsive">
|
|
55
|
+
<table class="table table-sm align-middle mb-0">
|
|
56
|
+
<thead>
|
|
57
|
+
<tr>
|
|
58
|
+
<th scope="col"><%= t('logs_recent_at', '시각') %></th>
|
|
59
|
+
<th scope="col"><%= t('logs_level', '레벨') %></th>
|
|
60
|
+
<th scope="col"><%= t('logs_message', '메시지') %></th>
|
|
61
|
+
<th scope="col"><%= t('logs_recent_trace', 'trace_id') %></th>
|
|
62
|
+
</tr>
|
|
63
|
+
</thead>
|
|
64
|
+
<tbody>
|
|
65
|
+
<% snap.recent.forEach(function (e) { %>
|
|
66
|
+
<tr>
|
|
67
|
+
<td class="font-monospace small"><%= fmt(e.at) %></td>
|
|
68
|
+
<td><span class="badge <%= levelBadge(e.level) %>"><%= e.level %></span></td>
|
|
69
|
+
<td class="small"><%= e.message %></td>
|
|
70
|
+
<td class="font-monospace small text-break"><%= e.traceId ? e.traceId.slice(0, 16) + '…' : '—' %></td>
|
|
71
|
+
</tr>
|
|
72
|
+
<% }) %>
|
|
73
|
+
</tbody>
|
|
74
|
+
</table>
|
|
75
|
+
</div>
|
|
76
|
+
<% } %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
<%
|
|
3
|
+
// 숫자 포맷 헬퍼 — 정수는 천단위 콤마, 소수는 1자리.
|
|
4
|
+
function n(v) { return Number(v).toLocaleString('en-US') }
|
|
5
|
+
function f1(v) { return Number(v).toFixed(1) }
|
|
6
|
+
function classBadge(c) { return c === '2xx' ? 'text-bg-success' : c === '3xx' ? 'text-bg-info' : c === '4xx' ? 'text-bg-warning' : 'text-bg-danger' }
|
|
7
|
+
// 잡 카운터는 키가 동적(metrics_jobs_<e>)이라 t() defaultValue 로 항목별 한국어 폴백을 직접 매핑한다.
|
|
8
|
+
const jobsLabel = { enqueued: '등록', processed: '처리', retried: '재시도', dlq: 'DLQ' }
|
|
9
|
+
%>
|
|
10
|
+
|
|
11
|
+
<div class="mb-4">
|
|
12
|
+
<h1 class="h3 mb-1"><%= t('metrics_title', '메트릭') %></h1>
|
|
13
|
+
<p class="text-body-secondary small mb-0"><%= t('metrics_subtitle', 'Prometheus /metrics 카운터를 사람이 보기 좋은 카드로 보여줍니다 (웹 프로세스 기준).') %></p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if (!snap.enabled) { %>
|
|
17
|
+
<div class="alert alert-warning" role="alert"><%= t('metrics_disabled', '메트릭이 비활성화되어 있습니다. mega.config.js 의 health.exposeMetrics 를 확인하세요.') %></div>
|
|
18
|
+
<% } else { %>
|
|
19
|
+
<div class="row g-3">
|
|
20
|
+
<div class="col-lg-6">
|
|
21
|
+
<div class="card h-100">
|
|
22
|
+
<div class="card-body">
|
|
23
|
+
<h2 class="h5 card-title"><%= t('metrics_http_title', 'HTTP 요청') %></h2>
|
|
24
|
+
<p class="card-text text-body-secondary small"><%= t('metrics_http_desc', '상태 코드 분류별 누적 요청 수입니다.') %></p>
|
|
25
|
+
<div class="display-5 fw-bold"><%= n(snap.http.total) %></div>
|
|
26
|
+
<div class="text-body-secondary small mb-3"><%= t('metrics_http_total', '총 요청 수') %></div>
|
|
27
|
+
<div class="d-flex flex-wrap gap-2">
|
|
28
|
+
<% Object.keys(snap.http.byClass).forEach(function (c) { %>
|
|
29
|
+
<span class="badge <%= classBadge(c) %>"><%= c %>: <%= n(snap.http.byClass[c]) %></span>
|
|
30
|
+
<% }) %>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="col-lg-6">
|
|
37
|
+
<div class="card h-100">
|
|
38
|
+
<div class="card-body">
|
|
39
|
+
<h2 class="h5 card-title"><%= t('metrics_process_title', '프로세스') %></h2>
|
|
40
|
+
<p class="card-text text-body-secondary small"><%= t('metrics_process_desc', '현재 웹 프로세스의 메모리·가동시간·CPU 사용량입니다.') %></p>
|
|
41
|
+
<ul class="list-group list-group-flush mt-1">
|
|
42
|
+
<li class="list-group-item d-flex justify-content-between px-0">
|
|
43
|
+
<span><%= t('metrics_process_heap', '힙 사용량') %></span><span class="font-monospace"><%= f1(snap.process.heapUsedMb) %> MB</span>
|
|
44
|
+
</li>
|
|
45
|
+
<li class="list-group-item d-flex justify-content-between px-0">
|
|
46
|
+
<span><%= t('metrics_process_rss', 'RSS') %></span><span class="font-monospace"><%= f1(snap.process.rssMb) %> MB</span>
|
|
47
|
+
</li>
|
|
48
|
+
<li class="list-group-item d-flex justify-content-between px-0">
|
|
49
|
+
<span><%= t('metrics_process_uptime', '가동 시간') %></span><span class="font-monospace"><%= n(Math.round(snap.process.uptimeSec)) %> s</span>
|
|
50
|
+
</li>
|
|
51
|
+
<li class="list-group-item d-flex justify-content-between px-0">
|
|
52
|
+
<span><%= t('metrics_process_cpu', 'CPU 시간') %></span><span class="font-monospace"><%= f1(snap.process.cpuSec) %> s</span>
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="col-lg-6">
|
|
60
|
+
<div class="card h-100">
|
|
61
|
+
<div class="card-body">
|
|
62
|
+
<h2 class="h5 card-title"><%= t('metrics_jobs_title', '잡 큐') %></h2>
|
|
63
|
+
<p class="card-text text-body-secondary small"><%= t('metrics_jobs_desc', '잡 처리 이벤트 누적 수입니다 (잡 데모와 연동).') %></p>
|
|
64
|
+
<div class="row text-center g-2 mt-1">
|
|
65
|
+
<% ['enqueued','processed','retried','dlq'].forEach(function (e) { %>
|
|
66
|
+
<div class="col-6 col-sm-3">
|
|
67
|
+
<div class="border rounded py-2">
|
|
68
|
+
<div class="h4 fw-bold mb-0"><%= n(snap.jobs[e]) %></div>
|
|
69
|
+
<div class="text-body-secondary small"><%= t('metrics_jobs_' + e, jobsLabel[e]) %></div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<% }) %>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="col-lg-6">
|
|
79
|
+
<div class="card h-100">
|
|
80
|
+
<div class="card-body">
|
|
81
|
+
<h2 class="h5 card-title"><%= t('metrics_ws_title', 'WebSocket') %></h2>
|
|
82
|
+
<p class="card-text text-body-secondary small"><%= t('metrics_ws_desc', 'WS 메시지 누적 수입니다 (채팅 데모와 연동).') %></p>
|
|
83
|
+
<div class="display-6 fw-bold"><%= n(snap.ws.total) %></div>
|
|
84
|
+
<div class="text-body-secondary small mb-2"><%= t('metrics_ws_total', '총 메시지 수') %></div>
|
|
85
|
+
<% if (snap.ws.byType.length > 0) { %>
|
|
86
|
+
<div class="d-flex flex-wrap gap-2">
|
|
87
|
+
<% snap.ws.byType.forEach(function (r) { %>
|
|
88
|
+
<span class="badge text-bg-light"><code><%= r.type %></code> <%= n(r.count) %></span>
|
|
89
|
+
<% }) %>
|
|
90
|
+
</div>
|
|
91
|
+
<% } else { %>
|
|
92
|
+
<p class="text-body-secondary small mb-0"><%= t('metrics_ws_empty', '아직 WS 메시지가 없습니다.') %></p>
|
|
93
|
+
<% } %>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="card mt-3">
|
|
100
|
+
<div class="card-body">
|
|
101
|
+
<h2 class="h5 card-title"><%= t('metrics_routes_title', '라우트별 요청 수') %></h2>
|
|
102
|
+
<p class="card-text text-body-secondary small"><%= t('metrics_routes_desc', '요청이 많은 상위 라우트입니다.') %></p>
|
|
103
|
+
<% if (snap.http.topRoutes.length === 0) { %>
|
|
104
|
+
<p class="text-body-secondary small mb-0"><%= t('metrics_routes_empty', '아직 기록된 요청이 없습니다.') %></p>
|
|
105
|
+
<% } else { %>
|
|
106
|
+
<div class="table-responsive">
|
|
107
|
+
<table class="table table-sm align-middle mb-0">
|
|
108
|
+
<thead><tr><th scope="col"><%= t('metrics_routes_route', '라우트') %></th><th scope="col" class="text-end"><%= t('metrics_routes_count', '요청 수') %></th></tr></thead>
|
|
109
|
+
<tbody>
|
|
110
|
+
<% snap.http.topRoutes.forEach(function (r) { %>
|
|
111
|
+
<tr><td class="font-monospace small"><%= r.route %></td><td class="text-end font-monospace"><%= n(r.count) %></td></tr>
|
|
112
|
+
<% }) %>
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
</div>
|
|
116
|
+
<% } %>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<% } %>
|
|
120
|
+
|
|
121
|
+
<p class="text-body-secondary small mt-3 mb-0">
|
|
122
|
+
<%= t('metrics_raw_hint', '원본(Prometheus scrape 포맷):') %> <a href="<%= snap.metricsPath %>" target="_blank" rel="noopener"><code><%= snap.metricsPath %></code></a>
|
|
123
|
+
</p>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="row justify-content-center">
|
|
4
|
+
<div class="col-lg-6">
|
|
5
|
+
<h1 class="h3 mb-4"><%= t('notes_edit_title', { defaultValue: '노트 수정' }) %></h1>
|
|
6
|
+
|
|
7
|
+
<% if (typeof error !== 'undefined' && error) { %>
|
|
8
|
+
<div class="alert alert-danger" role="alert"><%= error %></div>
|
|
9
|
+
<% } %>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="/demo/notes/<%= values.id %>" novalidate>
|
|
12
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
13
|
+
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="title" class="form-label"><%= t('notes_field_title', { defaultValue: '제목' }) %></label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
class="form-control <%= invalid && invalid.title ? 'is-invalid' : '' %>"
|
|
19
|
+
id="title"
|
|
20
|
+
name="title"
|
|
21
|
+
value="<%= values && values.title ? values.title : '' %>"
|
|
22
|
+
placeholder="<%= t('notes_field_title_ph', { defaultValue: '예: 회의 메모' }) %>"
|
|
23
|
+
required
|
|
24
|
+
/>
|
|
25
|
+
<div class="invalid-feedback"><%= t('notes_field_title_required', { defaultValue: '제목을 입력하세요.' }) %></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="mb-4">
|
|
29
|
+
<label for="body" class="form-label"><%= t('notes_field_body', { defaultValue: '본문' }) %></label>
|
|
30
|
+
<textarea
|
|
31
|
+
class="form-control"
|
|
32
|
+
id="body"
|
|
33
|
+
name="body"
|
|
34
|
+
rows="4"
|
|
35
|
+
placeholder="<%= t('notes_field_body_ph', { defaultValue: '내용을 입력하세요 (선택).' }) %>"
|
|
36
|
+
><%= values && values.body ? values.body : '' %></textarea>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="d-flex gap-2">
|
|
40
|
+
<button type="submit" class="btn btn-primary"><%= t('btn_save', { defaultValue: '저장' }) %></button>
|
|
41
|
+
<a href="/demo/notes" class="btn btn-outline-secondary"><%= t('btn_cancel', { defaultValue: '취소' }) %></a>
|
|
42
|
+
</div>
|
|
43
|
+
</form>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="h3 mb-1"><%= t('notes_title', { defaultValue: '노트 (MongoDB)' }) %></h1>
|
|
6
|
+
<p class="text-body-secondary small mb-0"><%= t('notes_subtitle', { defaultValue: 'MongoDB 도큐먼트 어댑터로 notes 컬렉션을 생성·조회·수정·삭제합니다.' }) %></p>
|
|
7
|
+
</div>
|
|
8
|
+
<a href="/demo/notes/new" class="btn btn-primary">+ <%= t('notes_new', { defaultValue: '노트 추가' }) %></a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if (typeof notice !== 'undefined' && notice) { %>
|
|
12
|
+
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
13
|
+
<%= notice %>
|
|
14
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
|
|
15
|
+
</div>
|
|
16
|
+
<% } %>
|
|
17
|
+
|
|
18
|
+
<% if (!notes || notes.length === 0) { %>
|
|
19
|
+
<div class="text-center text-body-secondary py-5">
|
|
20
|
+
<div class="display-4 mb-3">📝</div>
|
|
21
|
+
<p class="mb-3"><%= t('notes_empty', { defaultValue: '아직 노트가 없습니다.' }) %></p>
|
|
22
|
+
<a href="/demo/notes/new" class="btn btn-outline-primary"><%= t('notes_new', { defaultValue: '노트 추가' }) %></a>
|
|
23
|
+
</div>
|
|
24
|
+
<% } else { %>
|
|
25
|
+
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3">
|
|
26
|
+
<% notes.forEach(function (n) { %>
|
|
27
|
+
<div class="col">
|
|
28
|
+
<div class="card h-100">
|
|
29
|
+
<div class="card-body d-flex flex-column">
|
|
30
|
+
<h2 class="h5 card-title"><%= n.title %></h2>
|
|
31
|
+
<p class="card-text text-body-secondary flex-grow-1"><%= n.body || '—' %></p>
|
|
32
|
+
<div class="text-body-secondary small mb-3"><%= n.created_at %></div>
|
|
33
|
+
<div class="d-flex gap-2">
|
|
34
|
+
<a href="/demo/notes/<%= n.id %>/edit" class="btn btn-sm btn-outline-secondary"><%= t('action_edit', { defaultValue: '수정' }) %></a>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="btn btn-sm btn-outline-danger"
|
|
38
|
+
data-bs-toggle="modal"
|
|
39
|
+
data-bs-target="#deleteModal"
|
|
40
|
+
data-action="/demo/notes/<%= n.id %>/delete"
|
|
41
|
+
data-name="<%= n.title %>"
|
|
42
|
+
>
|
|
43
|
+
<%= t('action_delete', { defaultValue: '삭제' }) %>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<% }) %>
|
|
50
|
+
</div>
|
|
51
|
+
<% } %>
|
|
52
|
+
|
|
53
|
+
<%# 삭제 확인 모달 — users 목록과 동일 패턴. 클릭된 카드의 data-action/data-name 으로 app.js 가 form·문구를
|
|
54
|
+
채운다(show.bs.modal). 인라인 스크립트는 CSP(script-src 'self')에 막히므로 wiring 은 app.js(외부)에 둔다. %>
|
|
55
|
+
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
|
56
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
57
|
+
<div class="modal-content">
|
|
58
|
+
<div class="modal-header">
|
|
59
|
+
<h5 class="modal-title" id="deleteModalLabel"><%= t('delete_confirm_title', { defaultValue: '삭제 확인' }) %></h5>
|
|
60
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="modal-body">
|
|
63
|
+
<%= t('delete_confirm_body', { defaultValue: '정말 삭제할까요?' }) %> <strong id="deleteModalName"></strong>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="modal-footer">
|
|
66
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t('delete_confirm_cancel', { defaultValue: '취소' }) %></button>
|
|
67
|
+
<form id="deleteForm" method="post" class="d-inline">
|
|
68
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
69
|
+
<button type="submit" class="btn btn-danger"><%= t('delete_confirm_ok', { defaultValue: '삭제' }) %></button>
|
|
70
|
+
</form>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="row justify-content-center">
|
|
4
|
+
<div class="col-lg-6">
|
|
5
|
+
<h1 class="h3 mb-4"><%= t('notes_new_title', { defaultValue: '노트 추가' }) %></h1>
|
|
6
|
+
|
|
7
|
+
<% if (typeof error !== 'undefined' && error) { %>
|
|
8
|
+
<div class="alert alert-danger" role="alert"><%= error %></div>
|
|
9
|
+
<% } %>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="/demo/notes" novalidate>
|
|
12
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
13
|
+
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="title" class="form-label"><%= t('notes_field_title', { defaultValue: '제목' }) %></label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
class="form-control <%= invalid && invalid.title ? 'is-invalid' : '' %>"
|
|
19
|
+
id="title"
|
|
20
|
+
name="title"
|
|
21
|
+
value="<%= values && values.title ? values.title : '' %>"
|
|
22
|
+
placeholder="<%= t('notes_field_title_ph', { defaultValue: '예: 회의 메모' }) %>"
|
|
23
|
+
required
|
|
24
|
+
/>
|
|
25
|
+
<div class="invalid-feedback"><%= t('notes_field_title_required', { defaultValue: '제목을 입력하세요.' }) %></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="mb-4">
|
|
29
|
+
<label for="body" class="form-label"><%= t('notes_field_body', { defaultValue: '본문' }) %></label>
|
|
30
|
+
<textarea
|
|
31
|
+
class="form-control"
|
|
32
|
+
id="body"
|
|
33
|
+
name="body"
|
|
34
|
+
rows="4"
|
|
35
|
+
placeholder="<%= t('notes_field_body_ph', { defaultValue: '내용을 입력하세요 (선택).' }) %>"
|
|
36
|
+
><%= values && values.body ? values.body : '' %></textarea>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="d-flex gap-2">
|
|
40
|
+
<button type="submit" class="btn btn-primary"><%= t('btn_create', { defaultValue: '생성' }) %></button>
|
|
41
|
+
<a href="/demo/notes" class="btn btn-outline-secondary"><%= t('btn_cancel', { defaultValue: '취소' }) %></a>
|
|
42
|
+
</div>
|
|
43
|
+
</form>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-4">
|
|
4
|
+
<h1 class="h3 mb-1"><%= t('perf_title', '성능 벤치마크') %></h1>
|
|
5
|
+
<p class="text-body-secondary small mb-0"><%= t('perf_subtitle', '프레임워크 주요 표면(HTTP·암호화·DB·캐시·세션)의 처리량과 지연을 Node 내장 perf_hooks 로 직접 측정합니다.') %></p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="row g-3">
|
|
9
|
+
<div class="col-lg-5">
|
|
10
|
+
<div class="card h-100">
|
|
11
|
+
<div class="card-body d-flex flex-column">
|
|
12
|
+
<h2 class="h5 card-title"><%= t('perf_form_title', '벤치마크 실행') %></h2>
|
|
13
|
+
<p class="card-text text-body-secondary small"><%= t('perf_form_desc', '시나리오와 반복 횟수를 고르고 실행하면 per-iteration 지연을 모아 백분위수를 계산합니다.') %></p>
|
|
14
|
+
|
|
15
|
+
<div class="mb-3">
|
|
16
|
+
<label for="pf-scenario" class="form-label small"><%= t('perf_scenario_label', '시나리오') %></label>
|
|
17
|
+
<select class="form-select form-select-sm font-monospace" id="pf-scenario">
|
|
18
|
+
<% scenarios.forEach(function (s) { %>
|
|
19
|
+
<option value="<%= s %>"><%= s %></option>
|
|
20
|
+
<% }) %>
|
|
21
|
+
</select>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="row g-2 mb-3">
|
|
25
|
+
<div class="col-4">
|
|
26
|
+
<label for="pf-iterations" class="form-label small"><%= t('perf_iterations_label', '반복') %></label>
|
|
27
|
+
<input type="number" class="form-control form-control-sm" id="pf-iterations" value="1000" min="1" max="100000" step="100" />
|
|
28
|
+
</div>
|
|
29
|
+
<div class="col-4">
|
|
30
|
+
<label for="pf-concurrency" class="form-label small"><%= t('perf_concurrency_label', '동시성') %></label>
|
|
31
|
+
<input type="number" class="form-control form-control-sm" id="pf-concurrency" placeholder="auto" min="1" max="100" step="1" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="col-4">
|
|
34
|
+
<label for="pf-payload" class="form-label small"><%= t('perf_payload_label', '페이로드(byte)') %></label>
|
|
35
|
+
<input type="number" class="form-control form-control-sm" id="pf-payload" placeholder="0" min="0" max="1048576" step="64" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<p class="text-body-secondary small mb-3"><%= t('perf_limit_hint', '동시성·페이로드를 비우면 시나리오별 기본값을 씁니다. 안전을 위해 시나리오마다 반복·동시성 상한이 있으며, 초과하면 자동으로 줄이고 결과에 표시합니다.') %></p>
|
|
39
|
+
|
|
40
|
+
<div class="d-flex align-items-center gap-2">
|
|
41
|
+
<button type="button" class="btn btn-primary btn-sm" id="pf-run">
|
|
42
|
+
<span class="spinner-border spinner-border-sm d-none" id="pf-spinner" role="status" aria-hidden="true"></span>
|
|
43
|
+
<span id="pf-run-label"><%= t('perf_run_btn', '실행') %></span>
|
|
44
|
+
</button>
|
|
45
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="pf-clear"><%= t('perf_clear_btn', '결과 지우기') %></button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="alert alert-danger mt-3 d-none small" id="pf-error" role="alert">
|
|
49
|
+
<%= t('perf_error', '실행 실패:') %> <span id="pf-error-detail" class="font-monospace"></span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="col-lg-7">
|
|
56
|
+
<div class="card h-100">
|
|
57
|
+
<div class="card-body d-flex flex-column">
|
|
58
|
+
<h2 class="h5 card-title"><%= t('perf_results_title', '결과') %></h2>
|
|
59
|
+
<p class="card-text text-body-secondary small"><%= t('perf_results_desc', '실행할 때마다 맨 위에 한 줄씩 쌓입니다. 시간(ms)은 작을수록, RPS 는 클수록 좋습니다.') %></p>
|
|
60
|
+
|
|
61
|
+
<div class="table-responsive">
|
|
62
|
+
<table class="table table-sm table-striped align-middle small mb-0">
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th scope="col"><%= t('perf_col_scenario', '시나리오') %></th>
|
|
66
|
+
<th scope="col" class="text-end"><%= t('perf_col_iter', '반복') %></th>
|
|
67
|
+
<th scope="col" class="text-end"><%= t('perf_col_conc', '동시성') %></th>
|
|
68
|
+
<th scope="col" class="text-end"><%= t('perf_col_duration', '총시간(ms)') %></th>
|
|
69
|
+
<th scope="col" class="text-end"><%= t('perf_col_rps', 'RPS') %></th>
|
|
70
|
+
<th scope="col" class="text-end"><%= t('perf_col_avg', '평균(ms)') %></th>
|
|
71
|
+
<th scope="col" class="text-end">p50</th>
|
|
72
|
+
<th scope="col" class="text-end">p90</th>
|
|
73
|
+
<th scope="col" class="text-end">p95</th>
|
|
74
|
+
<th scope="col" class="text-end">p99</th>
|
|
75
|
+
<th scope="col" class="text-end"><%= t('perf_col_min', '최소') %></th>
|
|
76
|
+
<th scope="col" class="text-end"><%= t('perf_col_max', '최대') %></th>
|
|
77
|
+
<th scope="col" class="text-end"><%= t('perf_col_okfail', '성공/실패') %></th>
|
|
78
|
+
</tr>
|
|
79
|
+
</thead>
|
|
80
|
+
<tbody id="pf-results" data-empty="<%= t('perf_results_empty', '아직 실행한 결과가 없습니다.') %>">
|
|
81
|
+
<tr id="pf-empty"><td colspan="13" class="text-center text-body-secondary py-3"><%= t('perf_results_empty', '아직 실행한 결과가 없습니다.') %></td></tr>
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<script src="/static/js/perf.js"></script>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-4">
|
|
4
|
+
<h1 class="h3 mb-1"><%= t('redis_title', { defaultValue: 'Redis 데모' }) %></h1>
|
|
5
|
+
<p class="text-body-secondary small mb-0"><%= t('redis_subtitle', { defaultValue: 'Redis 어댑터로 방문 카운터(원자적 INCR/EXPIRE)와 쿼리 결과 캐시(GET/SET/TTL/DEL)를 시연합니다.' }) %></p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if (typeof notice !== 'undefined' && notice) { %>
|
|
9
|
+
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
10
|
+
<%= notice %>
|
|
11
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
|
|
12
|
+
</div>
|
|
13
|
+
<% } %>
|
|
14
|
+
|
|
15
|
+
<div class="row g-3">
|
|
16
|
+
<div class="col-lg-6">
|
|
17
|
+
<div class="card h-100">
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<h2 class="h5 card-title"><%= t('redis_visits_title', { defaultValue: '방문 카운터' }) %></h2>
|
|
20
|
+
<p class="card-text text-body-secondary small"><%= t('redis_visits_desc', { defaultValue: '이 페이지를 열 때마다 카운터를 원자적으로 1씩 올립니다. 당일 카운터는 2일 후 자동 만료됩니다.' }) %></p>
|
|
21
|
+
<div class="d-flex gap-4 mt-3">
|
|
22
|
+
<div>
|
|
23
|
+
<div class="display-6 fw-bold"><%= visits.total %></div>
|
|
24
|
+
<div class="text-body-secondary small"><%= t('redis_visits_total', { defaultValue: '누적 방문' }) %></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<div class="display-6 fw-bold"><%= visits.today %></div>
|
|
28
|
+
<div class="text-body-secondary small"><%= t('redis_visits_today', { defaultValue: '오늘 방문' }) %> (<%= visits.date %>)</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="mt-3"><code class="small">INCR · EXPIRE</code></div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="col-lg-6">
|
|
37
|
+
<div class="card h-100">
|
|
38
|
+
<div class="card-body d-flex flex-column">
|
|
39
|
+
<h2 class="h5 card-title"><%= t('redis_cache_title', { defaultValue: '쿼리 결과 캐시' }) %></h2>
|
|
40
|
+
<p class="card-text text-body-secondary small"><%= t('redis_cache_desc', { defaultValue: '사용자 수를 Redis 에 30초간 캐싱합니다. 캐시에 있으면 hit(SQL 미실행), 없으면 miss(SQL 재계산)입니다.' }) %></p>
|
|
41
|
+
<div class="d-flex align-items-center gap-3 mt-3">
|
|
42
|
+
<div class="display-6 fw-bold"><%= cacheState.value %></div>
|
|
43
|
+
<div>
|
|
44
|
+
<% if (cacheState.isHit) { %>
|
|
45
|
+
<span class="badge text-bg-success"><%= t('redis_cache_hit', { defaultValue: '캐시 적중' }) %></span>
|
|
46
|
+
<% } else { %>
|
|
47
|
+
<span class="badge text-bg-warning"><%= t('redis_cache_miss', { defaultValue: '캐시 미스' }) %></span>
|
|
48
|
+
<% } %>
|
|
49
|
+
<div class="text-body-secondary small mt-1">
|
|
50
|
+
<%= t('redis_cache_ttl', { defaultValue: '남은 TTL' }) %>: <%= cacheState.ttlSeconds %>s
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="mt-3"><code class="small">GET · SET EX · TTL · DEL</code></div>
|
|
55
|
+
<div class="mt-auto pt-3 d-flex gap-2">
|
|
56
|
+
<a href="/demo/redis" class="btn btn-outline-secondary btn-sm"><%= t('redis_cache_reload', { defaultValue: '다시 조회' }) %></a>
|
|
57
|
+
<form method="post" action="/demo/redis/clear" class="m-0">
|
|
58
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
59
|
+
<button type="submit" class="btn btn-outline-danger btn-sm"><%= t('redis_cache_clear', { defaultValue: '캐시 비우기' }) %></button>
|
|
60
|
+
</form>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<% layout('layouts/main') %>
|
|
2
|
+
<%
|
|
3
|
+
function fmt(d) { return new Date(d).toLocaleString('ko-KR', { hour12: false, timeZone: 'Asia/Seoul' }) }
|
|
4
|
+
function traceUrl(id) { return snap.zipkinBase + '/zipkin/traces/' + id }
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<div class="mb-4">
|
|
8
|
+
<h1 class="h3 mb-1"><%= t('tracing_title', '분산 추적') %></h1>
|
|
9
|
+
<p class="text-body-secondary small mb-0"><%= t('tracing_subtitle', '현재 요청의 trace_id 와 최근 trace 를 Zipkin 으로 잇습니다.') %></p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if (typeof notice !== 'undefined' && notice) { %>
|
|
13
|
+
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
14
|
+
<%= notice %>
|
|
15
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>
|
|
16
|
+
</div>
|
|
17
|
+
<% } %>
|
|
18
|
+
|
|
19
|
+
<% if (!snap.enabled) { %>
|
|
20
|
+
<div class="alert alert-warning" role="alert"><%= t('tracing_disabled', '트레이싱이 비활성화되어 있습니다. .env 의 MEGA_OTEL_ENABLED=true 후 재시작하세요.') %></div>
|
|
21
|
+
<% } %>
|
|
22
|
+
|
|
23
|
+
<div class="row g-3">
|
|
24
|
+
<div class="col-lg-6">
|
|
25
|
+
<div class="card h-100">
|
|
26
|
+
<div class="card-body d-flex flex-column">
|
|
27
|
+
<h2 class="h5 card-title"><%= t('tracing_current_title', '현재 요청 trace') %></h2>
|
|
28
|
+
<p class="card-text text-body-secondary small"><%= t('tracing_current_desc', '이 페이지를 연 요청의 trace_id 입니다 (응답 헤더 x-trace-id 에도 실립니다).') %></p>
|
|
29
|
+
<% if (snap.current) { %>
|
|
30
|
+
<div class="mt-2">
|
|
31
|
+
<div class="text-body-secondary small"><%= t('tracing_trace_id', 'trace_id') %></div>
|
|
32
|
+
<code class="user-select-all d-block text-break"><%= snap.current.traceId %></code>
|
|
33
|
+
<div class="text-body-secondary small mt-2"><%= t('tracing_span_id', 'span_id') %></div>
|
|
34
|
+
<code class="user-select-all d-block text-break"><%= snap.current.spanId %></code>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="mt-3">
|
|
37
|
+
<a class="btn btn-outline-primary btn-sm" href="<%= traceUrl(snap.current.traceId) %>" target="_blank" rel="noopener"><%= t('tracing_open_zipkin', 'Zipkin 에서 보기') %></a>
|
|
38
|
+
</div>
|
|
39
|
+
<% } else { %>
|
|
40
|
+
<p class="text-body-secondary small mb-0 mt-2"><%= t('tracing_no_current', '현재 trace 가 없습니다 (트레이싱 비활성).') %></p>
|
|
41
|
+
<% } %>
|
|
42
|
+
<div class="mt-auto pt-3">
|
|
43
|
+
<form method="post" action="/demo/tracing/generate" class="m-0">
|
|
44
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
45
|
+
<button type="submit" class="btn btn-primary btn-sm"><%= t('tracing_generate', 'trace 생성') %></button>
|
|
46
|
+
</form>
|
|
47
|
+
<p class="text-body-secondary small mb-0 mt-2"><%= t('tracing_generate_hint', '사용자 span 으로 DB 핑을 한 번 실행해 다층 span 트리를 만듭니다.') %></p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="col-lg-6">
|
|
54
|
+
<div class="card h-100">
|
|
55
|
+
<div class="card-body">
|
|
56
|
+
<h2 class="h5 card-title"><%= t('tracing_zipkin_title', 'Zipkin') %></h2>
|
|
57
|
+
<p class="card-text text-body-secondary small"><%= t('tracing_zipkin_desc', 'trace 는 OTLP→collector→Zipkin 으로 전달됩니다.') %></p>
|
|
58
|
+
<ul class="list-group list-group-flush mt-1">
|
|
59
|
+
<li class="list-group-item d-flex justify-content-between px-0">
|
|
60
|
+
<span><%= t('tracing_service', '서비스 이름') %></span><code class="font-monospace"><%= snap.serviceName %></code>
|
|
61
|
+
</li>
|
|
62
|
+
<li class="list-group-item d-flex justify-content-between px-0">
|
|
63
|
+
<span><%= t('tracing_ui', 'Zipkin UI') %></span>
|
|
64
|
+
<a href="<%= snap.zipkinBase %>" target="_blank" rel="noopener" class="font-monospace small"><%= snap.zipkinBase %></a>
|
|
65
|
+
</li>
|
|
66
|
+
</ul>
|
|
67
|
+
<p class="text-body-secondary small mt-3 mb-0"><%= t('tracing_export_hint', '전송에 약간의 지연이 있어, 생성 직후엔 Zipkin 에 잠시 뒤 나타납니다.') %></p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="card mt-3">
|
|
74
|
+
<div class="card-body">
|
|
75
|
+
<h2 class="h5 card-title"><%= t('tracing_recent_title', '최근 trace') %></h2>
|
|
76
|
+
<p class="card-text text-body-secondary small"><%= t('tracing_recent_desc', '최근 기록된 trace_id 목록입니다.') %></p>
|
|
77
|
+
<% if (snap.recent.length === 0) { %>
|
|
78
|
+
<p class="text-body-secondary small mb-0"><%= t('tracing_recent_empty', '아직 기록된 trace 가 없습니다.') %></p>
|
|
79
|
+
<% } else { %>
|
|
80
|
+
<div class="table-responsive">
|
|
81
|
+
<table class="table table-sm align-middle mb-0">
|
|
82
|
+
<thead>
|
|
83
|
+
<tr>
|
|
84
|
+
<th scope="col"><%= t('tracing_recent_at', '시각') %></th>
|
|
85
|
+
<th scope="col"><%= t('tracing_recent_route', '라우트') %></th>
|
|
86
|
+
<th scope="col"><%= t('tracing_trace_id', 'trace_id') %></th>
|
|
87
|
+
<th scope="col"></th>
|
|
88
|
+
</tr>
|
|
89
|
+
</thead>
|
|
90
|
+
<tbody>
|
|
91
|
+
<% snap.recent.forEach(function (e) { %>
|
|
92
|
+
<tr>
|
|
93
|
+
<td class="font-monospace small"><%= fmt(e.at) %></td>
|
|
94
|
+
<td class="font-monospace small"><%= e.route %></td>
|
|
95
|
+
<td class="font-monospace small text-break"><%= e.traceId %></td>
|
|
96
|
+
<td class="text-end">
|
|
97
|
+
<a class="btn btn-outline-secondary btn-sm" href="<%= traceUrl(e.traceId) %>" target="_blank" rel="noopener"><%= t('tracing_view', '보기') %></a>
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
<% }) %>
|
|
101
|
+
</tbody>
|
|
102
|
+
</table>
|
|
103
|
+
</div>
|
|
104
|
+
<% } %>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|