From 3b5c3fb86f16843d02cd476d622ad1a3f0f0f0b6 Mon Sep 17 00:00:00 2001 From: andreferraro Date: Tue, 11 Aug 2026 14:58:33 -0300 Subject: [PATCH] feat: add lab SQLite migrate, LiteLLM env, and OTEL/Grafana stack Enable local workshop readiness with executable SQLite migrations, OpenTelemetry hello signals, and a provisioned Grafana demo dashboard over Tempo/Loki/Prometheus. Co-authored-by: Cursor --- .env.example | 13 + .gitignore | 5 + README.md | 43 +- docker-compose.yaml | 72 ++ migrations/001_tickets.sql | 27 + .../provisioning/dashboards/dashboards.yml | 12 + .../dashboards/json/ticketlab-demo.json | 155 ++++ .../provisioning/datasources/datasources.yaml | 43 ++ observability/loki.yaml | 35 + observability/otel-collector-config.yaml | 41 + observability/prometheus.yml | 14 + observability/tempo.yaml | 39 + package-lock.json | 722 ++++++++++++++++++ package.json | 18 +- src/db/migrate.test.ts | 29 + src/db/migrate.ts | 75 ++ src/env.ts | 20 + src/hello.ts | 37 +- src/telemetry.ts | 93 +++ 19 files changed, 1487 insertions(+), 6 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.yaml create mode 100644 migrations/001_tickets.sql create mode 100644 observability/grafana/provisioning/dashboards/dashboards.yml create mode 100644 observability/grafana/provisioning/dashboards/json/ticketlab-demo.json create mode 100644 observability/grafana/provisioning/datasources/datasources.yaml create mode 100644 observability/loki.yaml create mode 100644 observability/otel-collector-config.yaml create mode 100644 observability/prometheus.yml create mode 100644 observability/tempo.yaml create mode 100644 src/db/migrate.test.ts create mode 100644 src/db/migrate.ts create mode 100644 src/env.ts create mode 100644 src/telemetry.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..73c5610 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# LiteLLM (OpenAI-compatible) +LITELLM_BASE_URL=https://litellm.app.andreferraro.com/v1 +LITELLM_MODEL=code +LITELLM_API_KEY= + +# SQLite local +SQLITE_PATH=data/ticketlab.sqlite + +# App → lab observability stack (docker compose) +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +OTEL_SERVICE_NAME=ticketlab +# Prometheus scrapes host:3000/metrics via host.docker.internal +PORT=3000 diff --git a/.gitignore b/.gitignore index 3ab0692..515425a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,11 @@ *.key node_modules/ dist/ +data/ +*.sqlite +*.sqlite-journal +*.sqlite-wal +*.sqlite-shm .scannerwork/ graphify-out/ _plane* diff --git a/README.md b/README.md index 7f95982..38fa422 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,55 @@ Hello world — repositório de testes MCP do TicketLab. ```bash npm ci +cp .env.example .env # preencha LITELLM_API_KEY +npm run migrate npm start ``` -Requires Node.js 22+. +Requires Node.js 22+ and npm (`package-lock.json`). + +## SQLite + +```bash +npm run migrate +``` + +Cria/atualiza `SQLITE_PATH` (default `data/ticketlab.sqlite`) com a migration `migrations/001_tickets.sql`. + +## LiteLLM + +Configure no `.env` (veja `.env.example`): + +- `LITELLM_BASE_URL` +- `LITELLM_MODEL` +- `LITELLM_API_KEY` + +## Observability lab stack + +Local Grafana + Tempo + Loki + OTEL Collector (+ Prometheus): + +```bash +docker compose up -d +``` + +| What | Where | +|------|--------| +| Grafana UI | http://localhost:3001 (`admin` / `admin`) | +| Demo dashboard | http://localhost:3001/d/ticketlab-demo (folder TicketLab; starred) | +| OTLP HTTP | `http://localhost:4318` | +| OTLP gRPC | `localhost:4317` | +| Tempo | http://localhost:3200 | +| Loki | http://localhost:3100 | +| Prometheus | http://localhost:9090 | + +Copy `.env.example` → `.env` for LiteLLM, SQLite e OTLP. Datasources (Prometheus, Loki, Tempo) are provisioned automatically. Stop with `docker compose down`. ## Scripts - `npm run build` — compile TypeScript to `dist/` -- `npm start` — run the compiled hello program +- `npm start` — hello world with OTLP (trace → Tempo, log → Loki, metric → Prometheus) - `npm run typecheck` — type-check without emitting +- `npm run migrate` — apply SQLite migrations - `npm test` — build and run tests ## CI / quality gate diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2d26b26 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,72 @@ +# TicketLab lab observability — Grafana + Tempo + Loki + OTEL Collector (+ Prometheus) +# Start: docker compose up -d +# UI: http://localhost:3001 (admin / admin) +# OTLP: http://localhost:4318 (HTTP) | localhost:4317 (gRPC) + +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:0.120.0 + command: ["--config=/etc/otelcol/config.yaml"] + volumes: + - ./observability/otel-collector-config.yaml:/etc/otelcol/config.yaml:ro + ports: + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + - "8889:8889" # collector Prometheus metrics (optional) + depends_on: + - tempo + - loki + - prometheus + + tempo: + image: grafana/tempo:2.7.1 + command: ["-config.file=/etc/tempo/tempo.yaml"] + volumes: + - ./observability/tempo.yaml:/etc/tempo/tempo.yaml:ro + - tempo-data:/var/tempo + ports: + - "3200:3200" # Tempo HTTP / API + + loki: + image: grafana/loki:3.4.2 + command: ["-config.file=/etc/loki/loki.yaml"] + volumes: + - ./observability/loki.yaml:/etc/loki/loki.yaml:ro + - loki-data:/loki + ports: + - "3100:3100" + + prometheus: + image: prom/prometheus:v3.2.1 + volumes: + - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + ports: + - "9090:9090" + # Host scrape: app on the host publishing GET /metrics + extra_hosts: + - "host.docker.internal:host-gateway" + + grafana: + image: grafana/grafana:11.5.2 + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer + GF_USERS_DEFAULT_THEME: dark + volumes: + - ./observability/grafana/provisioning:/etc/grafana/provisioning:ro + - grafana-data:/var/lib/grafana + ports: + - "3001:3000" + depends_on: + - tempo + - loki + - prometheus + +volumes: + tempo-data: + loki-data: + prometheus-data: + grafana-data: diff --git a/migrations/001_tickets.sql b/migrations/001_tickets.sql new file mode 100644 index 0000000..6495052 --- /dev/null +++ b/migrations/001_tickets.sql @@ -0,0 +1,27 @@ +-- TicketLab initial schema (lab / walking skeleton) +CREATE TABLE IF NOT EXISTS schema_migrations ( + id TEXT PRIMARY KEY NOT NULL, + applied_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS tickets ( + id TEXT PRIMARY KEY NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('PENDING', 'CLASSIFIED', 'FAILED')), + category TEXT CHECK ( + category IS NULL + OR category IN ('INCIDENT', 'ACCESS_REQUEST', 'BILLING', 'FEATURE_REQUEST', 'OTHER') + ), + confidence REAL CHECK ( + confidence IS NULL OR (confidence >= 0 AND confidence <= 1) + ), + failure_reason TEXT CHECK ( + failure_reason IS NULL + OR failure_reason IN ('AI_TIMEOUT', 'AI_UNAVAILABLE', 'AI_INVALID_RESPONSE') + ), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets (status); +CREATE INDEX IF NOT EXISTS idx_tickets_created_at ON tickets (created_at); diff --git a/observability/grafana/provisioning/dashboards/dashboards.yml b/observability/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..392cb33 --- /dev/null +++ b/observability/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: ticketlab + orgId: 1 + folder: TicketLab + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /etc/grafana/provisioning/dashboards/json diff --git a/observability/grafana/provisioning/dashboards/json/ticketlab-demo.json b/observability/grafana/provisioning/dashboards/json/ticketlab-demo.json new file mode 100644 index 0000000..6c45e68 --- /dev/null +++ b/observability/grafana/provisioning/dashboards/json/ticketlab-demo.json @@ -0,0 +1,155 @@ +{ + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "type": "row", + "title": "Metrics", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 10, + "collapsed": false + }, + { + "id": 1, + "title": "Hello runs (total)", + "type": "stat", + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 1 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "targets": [ + { + "refId": "A", + "expr": "otelcol_ticketlab_hello_runs_total{exported_job=\"ticketlab\"}", + "legendFormat": "runs" + } + ], + "options": { + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "blue", "value": null }, + { "color": "green", "value": 1 } + ] + }, + "unit": "short" + }, + "overrides": [] + } + }, + { + "id": 2, + "title": "Hello runs over time", + "type": "timeseries", + "gridPos": { "h": 6, "w": 18, "x": 6, "y": 1 }, + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "targets": [ + { + "refId": "A", + "expr": "otelcol_ticketlab_hello_runs_total{exported_job=\"ticketlab\"}", + "legendFormat": "{{component}}" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 15, + "showPoints": "auto", + "spanNulls": true + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + } + }, + { + "type": "row", + "title": "Logs", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 7 }, + "id": 11, + "collapsed": false + }, + { + "id": 3, + "title": "TicketLab logs (Loki)", + "type": "logs", + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 8 }, + "datasource": { "type": "loki", "uid": "loki" }, + "targets": [ + { + "refId": "A", + "expr": "{service_name=\"ticketlab\"}", + "queryType": "range" + } + ], + "options": { + "showTime": true, + "showLabels": true, + "showCommonLabels": false, + "wrapLogMessage": true, + "prettifyLogMessage": true, + "enableLogDetails": true, + "dedupStrategy": "none", + "sortOrder": "Descending" + } + }, + { + "type": "row", + "title": "Tracing", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 18 }, + "id": 12, + "collapsed": false + }, + { + "id": 4, + "title": "Recent traces (Tempo)", + "type": "table", + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 19 }, + "datasource": { "type": "tempo", "uid": "tempo" }, + "targets": [ + { + "refId": "A", + "datasource": { "type": "tempo", "uid": "tempo" }, + "queryType": "traceql", + "query": "{ resource.service.name = \"ticketlab\" }", + "limit": 20, + "tableType": "traces" + } + ], + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { "show": false } + } + } + ], + "refresh": "5s", + "schemaVersion": 39, + "tags": ["ticketlab", "otel", "demo"], + "templating": { "list": [] }, + "time": { "from": "now-1h", "to": "now" }, + "timepicker": {}, + "timezone": "browser", + "title": "TicketLab Demo - Metrics / Logs / Traces", + "uid": "ticketlab-demo", + "version": 1 +} diff --git a/observability/grafana/provisioning/datasources/datasources.yaml b/observability/grafana/provisioning/datasources/datasources.yaml new file mode 100644 index 0000000..988749f --- /dev/null +++ b/observability/grafana/provisioning/datasources/datasources.yaml @@ -0,0 +1,43 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false + + - name: Loki + type: loki + uid: loki + access: proxy + url: http://loki:3100 + editable: false + jsonData: + derivedFields: + - datasourceUid: tempo + matcherRegex: '"trace_id":"(\w+)"' + name: TraceID + url: "$${__value.raw}" + urlDisplayLabel: View trace + + - name: Tempo + type: tempo + uid: tempo + access: proxy + url: http://tempo:3200 + editable: false + jsonData: + tracesToLogsV2: + datasourceUid: loki + spanStartTimeShift: "-1h" + spanEndTimeShift: "1h" + filterByTraceID: true + serviceMap: + datasourceUid: prometheus + nodeGraph: + enabled: true + lokiSearch: + datasourceUid: loki diff --git a/observability/loki.yaml b/observability/loki.yaml new file mode 100644 index 0000000..c32e2f5 --- /dev/null +++ b/observability/loki.yaml @@ -0,0 +1,35 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + instance_addr: 127.0.0.1 + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + filesystem: + directory: /loki/chunks + +limits_config: + allow_structured_metadata: true + reject_old_samples: true + reject_old_samples_max_age: 168h diff --git a/observability/otel-collector-config.yaml b/observability/otel-collector-config.yaml new file mode 100644 index 0000000..fe87d5c --- /dev/null +++ b/observability/otel-collector-config.yaml @@ -0,0 +1,41 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: {} + +exporters: + otlp/tempo: + endpoint: tempo:4317 + tls: + insecure: true + + loki: + endpoint: http://loki:3100/loki/api/v1/push + + prometheus: + endpoint: 0.0.0.0:8889 + namespace: otelcol + + debug: + verbosity: basic + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp/tempo, debug] + logs: + receivers: [otlp] + processors: [batch] + exporters: [loki, debug] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus, debug] diff --git a/observability/prometheus.yml b/observability/prometheus.yml new file mode 100644 index 0000000..a2de4f6 --- /dev/null +++ b/observability/prometheus.yml @@ -0,0 +1,14 @@ +global: + scrape_interval: 15s + +scrape_configs: + # App TicketLab on the host (when GET /metrics exists) + - job_name: ticketlab + metrics_path: /metrics + static_configs: + - targets: ["host.docker.internal:3000"] + + # OTEL Collector internal metrics + - job_name: otel-collector + static_configs: + - targets: ["otel-collector:8889"] diff --git a/observability/tempo.yaml b/observability/tempo.yaml new file mode 100644 index 0000000..2bb5a80 --- /dev/null +++ b/observability/tempo.yaml @@ -0,0 +1,39 @@ +server: + http_listen_port: 3200 + +distributor: + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +ingester: + trace_idle_period: 10s + max_block_duration: 5m + +compactor: + compaction: + block_retention: 24h + +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + local: + path: /var/tempo/blocks + +metrics_generator: + registry: + external_labels: + source: tempo + storage: + path: /var/tempo/generator/wal + +overrides: + defaults: + metrics_generator: + processors: [service-graphs, span-metrics] diff --git a/package-lock.json b/package-lock.json index fd05137..b50b1a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,22 @@ "": { "name": "ticketlab-mcp", "version": "0.1.0", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@opentelemetry/sdk-trace-node": "^2.10.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "better-sqlite3": "^11.9.1" + }, "devDependencies": { + "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.13.10", "typescript": "^5.8.2" }, @@ -15,6 +30,264 @@ "node": ">=22" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.221.0.tgz", + "integrity": "sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.221.0.tgz", + "integrity": "sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", + "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -25,6 +298,443 @@ "undici-types": "~6.21.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -45,6 +755,18 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/package.json b/package.json index e09c6f0..0b8a444 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,25 @@ "build": "tsc", "start": "node dist/hello.js", "typecheck": "tsc --noEmit", - "test": "npm run build && node --test dist/hello.test.js" + "migrate": "npm run build && node dist/db/migrate.js", + "test": "npm run build && node --test dist/**/*.test.js" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@opentelemetry/sdk-trace-node": "^2.10.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "better-sqlite3": "^11.9.1" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.13.10", "typescript": "^5.8.2" } diff --git a/src/db/migrate.test.ts b/src/db/migrate.test.ts new file mode 100644 index 0000000..ace42ab --- /dev/null +++ b/src/db/migrate.test.ts @@ -0,0 +1,29 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import assert from "node:assert/strict"; +import test from "node:test"; +import Database from "better-sqlite3"; +import { migrate } from "./migrate.js"; + +test("migrate creates tickets table on a fresh sqlite file", () => { + const dir = mkdtempSync(join(tmpdir(), "ticketlab-migrate-")); + const dbPath = join(dir, "test.sqlite"); + try { + const path = migrate(dbPath); + assert.equal(path, dbPath); + const db = new Database(dbPath, { readonly: true }); + try { + const row = db + .prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'tickets'`, + ) + .get() as { name: string } | undefined; + assert.equal(row?.name, "tickets"); + } finally { + db.close(); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/src/db/migrate.ts b/src/db/migrate.ts new file mode 100644 index 0000000..d2655f4 --- /dev/null +++ b/src/db/migrate.ts @@ -0,0 +1,75 @@ +import { mkdirSync, readdirSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import Database from "better-sqlite3"; +import { loadDotEnv } from "../env.js"; + +export { loadDotEnv } from "../env.js"; + +function assertTicketsTable(db: Database.Database): void { + const row = db + .prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'tickets'`, + ) + .get() as { name: string } | undefined; + if (!row) { + throw new Error("migration check failed: tickets table missing"); + } +} + +export function migrate( + dbPath = process.env.SQLITE_PATH ?? "data/ticketlab.sqlite", + migrationsDir = join(process.cwd(), "migrations"), +): string { + mkdirSync(dirname(dbPath), { recursive: true }); + const db = new Database(dbPath); + try { + db.exec(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + id TEXT PRIMARY KEY NOT NULL, + applied_at TEXT NOT NULL + ); + `); + + const files = readdirSync(migrationsDir) + .filter((f) => f.endsWith(".sql")) + .sort((a, b) => a.localeCompare(b)); + + const applied = new Set( + ( + db.prepare(`SELECT id FROM schema_migrations`).all() as Array<{ + id: string; + }> + ).map((r) => r.id), + ); + + const insert = db.prepare( + `INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)`, + ); + + const run = db.transaction((sqlFiles: string[]) => { + for (const file of sqlFiles) { + if (applied.has(file)) continue; + const sql = readFileSync(join(migrationsDir, file), "utf8"); + db.exec(sql); + insert.run(file, new Date().toISOString()); + } + }); + + run(files); + assertTicketsTable(db); + return dbPath; + } finally { + db.close(); + } +} + +const isMain = + process.argv[1] !== undefined && + resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1]); + +if (isMain) { + loadDotEnv(); + const path = migrate(); + console.log(`SQLite ready: ${path}`); +} diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..e8fe053 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** Load `.env` into process.env without overriding existing vars. */ +export function loadDotEnv(cwd = process.cwd()): void { + try { + const text = readFileSync(join(cwd, ".env"), "utf8"); + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const i = trimmed.indexOf("="); + if (i <= 0) continue; + const key = trimmed.slice(0, i).trim(); + const value = trimmed.slice(i + 1).trim(); + if (process.env[key] === undefined) process.env[key] = value; + } + } catch { + // optional .env + } +} diff --git a/src/hello.ts b/src/hello.ts index ea657ed..90f6171 100644 --- a/src/hello.ts +++ b/src/hello.ts @@ -1,4 +1,6 @@ import { pathToFileURL } from "node:url"; +import { loadDotEnv } from "./env.js"; +import { SeverityNumber, startTelemetry } from "./telemetry.js"; /** Hello world for TicketLab_MCP. */ @@ -7,8 +9,32 @@ export function greet(name: string = "TicketLab MCP"): string { return `Hello, ${name}!`; } -export function main(): void { - console.log(greet()); +/** Run hello with OTLP trace + metric + log (Grafana / Tempo / Loki / Prometheus). */ +export async function main(): Promise { + loadDotEnv(); + const telemetry = startTelemetry(); + const runs = telemetry.meter.createCounter("ticketlab_hello_runs_total", { + description: "Hello world invocations", + }); + + await telemetry.tracer.startActiveSpan("ticketlab.hello", async (span) => { + const message = greet(); + runs.add(1, { component: "hello" }); + telemetry.logger.emit({ + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + body: message, + attributes: { + "ticketlab.event": "hello", + "trace_id": span.spanContext().traceId, + }, + }); + console.log(message); + span.setAttribute("ticketlab.greeting", message); + span.end(); + }); + + await telemetry.shutdown(); } const entry = process.argv[1]; @@ -16,5 +42,10 @@ const isMain = entry !== undefined && import.meta.url === pathToFileURL(entry).href; if (isMain) { - main(); + try { + await main(); + } catch (err) { + console.error(err); + process.exitCode = 1; + } } diff --git a/src/telemetry.ts b/src/telemetry.ts new file mode 100644 index 0000000..6bbcbd1 --- /dev/null +++ b/src/telemetry.ts @@ -0,0 +1,93 @@ +import { metrics, trace } from "@opentelemetry/api"; +import { logs } from "@opentelemetry/api-logs"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { + LoggerProvider, + SimpleLogRecordProcessor, +} from "@opentelemetry/sdk-logs"; +import { + MeterProvider, + PeriodicExportingMetricReader, +} from "@opentelemetry/sdk-metrics"; +import { + NodeTracerProvider, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-node"; +import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; + +export type Telemetry = { + tracer: ReturnType; + meter: ReturnType; + logger: ReturnType; + shutdown: () => Promise; +}; + +function otlpBaseUrl(): string { + return ( + process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.replace(/\/$/, "") ?? + "http://localhost:4318" + ); +} + +export function startTelemetry( + serviceName = process.env.OTEL_SERVICE_NAME ?? "ticketlab", +): Telemetry { + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: serviceName, + }); + const base = otlpBaseUrl(); + + const tracerProvider = new NodeTracerProvider({ + resource, + spanProcessors: [ + new SimpleSpanProcessor( + new OTLPTraceExporter({ url: `${base}/v1/traces` }), + ), + ], + }); + tracerProvider.register(); + + const meterProvider = new MeterProvider({ + resource, + readers: [ + new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ url: `${base}/v1/metrics` }), + exportIntervalMillis: 500, + }), + ], + }); + metrics.setGlobalMeterProvider(meterProvider); + + const loggerProvider = new LoggerProvider({ + resource, + processors: [ + new SimpleLogRecordProcessor({ + exporter: new OTLPLogExporter({ url: `${base}/v1/logs` }), + }), + ], + }); + logs.setGlobalLoggerProvider(loggerProvider); + + return { + tracer: trace.getTracer(serviceName), + meter: metrics.getMeter(serviceName), + logger: logs.getLogger(serviceName), + shutdown: async () => { + await Promise.all([ + tracerProvider.forceFlush(), + meterProvider.forceFlush(), + loggerProvider.forceFlush(), + ]); + await Promise.all([ + tracerProvider.shutdown(), + meterProvider.shutdown(), + loggerProvider.shutdown(), + ]); + }, + }; +} + +export { SeverityNumber } from "@opentelemetry/api-logs";