Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

Add Grafana integration for metrics and logs persistence

nekomimi.pet 2ad29785 e064c292

verified
Changed files
+1558 -10
apps
hosting-service
src
main-app
src
docs
packages
+72
.env.grafana.example
···
+
# Grafana Cloud Configuration for wisp.place monorepo
+
# Copy these variables to your .env file to enable Grafana integration
+
# The observability package will automatically pick up these environment variables
+
+
# ============================================================================
+
# Grafana Loki (for logs)
+
# ============================================================================
+
# Get this from your Grafana Cloud portal under Loki → Details
+
# Example: https://logs-prod-012.grafana.net
+
GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net
+
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
+
GRAFANA_LOKI_TOKEN=glc_xxx
+
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
+
# GRAFANA_LOKI_USERNAME=your-username
+
# GRAFANA_LOKI_PASSWORD=your-password
+
+
# ============================================================================
+
# Grafana Prometheus (for metrics)
+
# ============================================================================
+
# Get this from your Grafana Cloud portal under Prometheus → Details
+
# Note: You need to add /api/prom to the base URL for OTLP export
+
# Example: https://prometheus-prod-10-prod-us-central-0.grafana.net/api/prom
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom
+
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
+
GRAFANA_PROMETHEUS_TOKEN=glc_xxx
+
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
+
# GRAFANA_PROMETHEUS_USERNAME=your-username
+
# GRAFANA_PROMETHEUS_PASSWORD=your-password
+
+
# ============================================================================
+
# Optional Configuration
+
# ============================================================================
+
# These will be used by both main-app and hosting-service if not overridden
+
+
# Service metadata (optional - defaults are provided in code)
+
# SERVICE_NAME=wisp-app
+
# SERVICE_VERSION=1.0.0
+
+
# Batching configuration (optional)
+
# GRAFANA_BATCH_SIZE=100 # Flush after this many entries
+
# GRAFANA_FLUSH_INTERVAL=5000 # Flush every 5 seconds
+
+
# ============================================================================
+
# How to get these values:
+
# ============================================================================
+
# 1. Sign up for Grafana Cloud at https://grafana.com/
+
# 2. Go to your Grafana Cloud portal
+
# 3. For Loki:
+
# - Navigate to "Connections" → "Loki"
+
# - Click "Details"
+
# - Copy the Push endpoint URL (without /loki/api/v1/push)
+
# - Create an API token with push permissions
+
# 4. For Prometheus:
+
# - Navigate to "Connections" → "Prometheus"
+
# - Click "Details"
+
# - Copy the Remote Write endpoint (add /api/prom for OTLP)
+
# - Create an API token with write permissions
+
+
# ============================================================================
+
# Testing the integration:
+
# ============================================================================
+
# 1. Copy this file's contents to your .env file
+
# 2. Fill in the actual values
+
# 3. Restart your services (main-app and hosting-service)
+
# 4. Check your Grafana Cloud dashboard for incoming data
+
# 5. Use Grafana Explore to query:
+
# - Loki: {job="main-app"} or {job="hosting-service"}
+
# - Prometheus: http_requests_total{service="main-app"}
+7 -1
apps/hosting-service/src/index.ts
···
import app from './server';
import { serve } from '@hono/node-server';
import { FirehoseWorker } from './lib/firehose';
-
import { createLogger } from '@wisp/observability';
+
import { createLogger, initializeGrafanaExporters } from '@wisp/observability';
import { mkdirSync, existsSync } from 'fs';
import { backfillCache } from './lib/backfill';
import { startDomainCacheCleanup, stopDomainCacheCleanup, setCacheOnlyMode } from './lib/db';
+
+
// Initialize Grafana exporters if configured
+
initializeGrafanaExporters({
+
serviceName: 'hosting-service',
+
serviceVersion: '1.0.0'
+
});
const logger = createLogger('hosting-service');
+7 -1
apps/main-app/src/index.ts
···
import { siteRoutes } from './routes/site'
import { csrfProtection } from './lib/csrf'
import { DNSVerificationWorker } from './lib/dns-verification-worker'
-
import { createLogger, logCollector } from '@wisp/observability'
+
import { createLogger, logCollector, initializeGrafanaExporters } from '@wisp/observability'
import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
import { promptAdminSetup } from './lib/admin-auth'
import { adminRoutes } from './routes/admin'
+
+
// Initialize Grafana exporters if configured
+
initializeGrafanaExporters({
+
serviceName: 'main-app',
+
serviceVersion: '1.0.50'
+
})
const logger = createLogger('main-app')
+179 -7
bun.lock
···
"packages/@wisp/observability": {
"name": "@wisp/observability",
"version": "1.0.0",
+
"dependencies": {
+
"@opentelemetry/api": "^1.9.0",
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
+
"@opentelemetry/resources": "^1.29.0",
+
"@opentelemetry/sdk-metrics": "^1.29.0",
+
"@opentelemetry/semantic-conventions": "^1.29.0",
+
},
+
"devDependencies": {
+
"@hono/node-server": "^1.19.6",
+
"bun-types": "^1.3.3",
+
"typescript": "^5.9.3",
+
},
"peerDependencies": {
-
"hono": "^4.0.0",
+
"hono": "",
},
"optionalPeers": [
"hono",
···
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
-
"@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
"@opentelemetry/core": ["@opentelemetry/core@1.29.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA=="],
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="],
···
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="],
-
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
+
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-exporter-base": "0.56.0", "@opentelemetry/otlp-transformer": "0.56.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/sdk-metrics": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GD5QuCT6js+mDpb5OBO6OSyCH+k2Gy3xPHJV9BnjV8W6kpSuY8y2Samzs5vl23UcGMq6sHLAbs+Eq/VYsLMiVw=="],
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="],
···
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
-
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="],
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
-
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.56.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.56.0", "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/sdk-logs": "0.56.0", "@opentelemetry/sdk-metrics": "1.29.0", "@opentelemetry/sdk-trace-base": "1.29.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ=="],
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
-
"@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
"@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="],
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="],
-
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog=="],
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
···
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
+
"@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
+
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="],
+
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="],
+
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
+
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-prometheus/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.56.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.56.0", "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ=="],
+
+
"@opentelemetry/propagator-b3/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/propagator-jaeger/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
+
+
"@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
+
+
"@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
+
+
"@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
+
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
+
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
···
"wisp-hosting-service/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="],
"@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="],
+
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
+
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/sdk-metrics/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
+
+
"@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+1
docs/astro.config.mjs
···
label: 'Guides',
items: [
{ label: 'Self-Hosting', slug: 'deployment' },
+
{ label: 'Monitoring & Metrics', slug: 'monitoring' },
{ label: 'Redirects & Rewrites', slug: 'redirects' },
],
},
+85
docs/src/content/docs/guides/grafana-setup.md
···
+
---
+
title: Grafana Setup Example
+
description: Quick setup for Grafana Cloud monitoring
+
---
+
+
Example setup for monitoring Wisp.place with Grafana Cloud.
+
+
## 1. Create Grafana Cloud Account
+
+
Sign up at [grafana.com](https://grafana.com) for a free tier account.
+
+
## 2. Get Credentials
+
+
Navigate to your stack and find:
+
+
**Loki** (Connections → Loki → Details):
+
- Push endpoint: `https://logs-prod-XXX.grafana.net`
+
- Create API token with write permissions
+
+
**Prometheus** (Connections → Prometheus → Details):
+
- Remote Write endpoint: `https://prometheus-prod-XXX.grafana.net/api/prom`
+
- Create API token with write permissions
+
+
## 3. Configure Wisp.place
+
+
Add to your `.env`:
+
+
```bash
+
GRAFANA_LOKI_URL=https://logs-prod-XXX.grafana.net
+
GRAFANA_LOKI_TOKEN=glc_eyJ...
+
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-XXX.grafana.net/api/prom
+
GRAFANA_PROMETHEUS_TOKEN=glc_eyJ...
+
```
+
+
## 4. Create Dashboard
+
+
Import this dashboard JSON or build your own:
+
+
```json
+
{
+
"panels": [
+
{
+
"title": "Request Rate",
+
"targets": [{
+
"expr": "sum(rate(http_requests_total[1m])) by (service)"
+
}]
+
},
+
{
+
"title": "P95 Latency",
+
"targets": [{
+
"expr": "histogram_quantile(0.95, rate(http_request_duration_ms_bucket[5m]))"
+
}]
+
},
+
{
+
"title": "Error Rate",
+
"targets": [{
+
"expr": "sum(rate(errors_total[5m])) / sum(rate(http_requests_total[5m]))"
+
}]
+
}
+
]
+
}
+
```
+
+
## 5. Set Alerts
+
+
Example alert for high error rate:
+
+
```yaml
+
alert: HighErrorRate
+
expr: |
+
sum(rate(errors_total[5m])) by (service) /
+
sum(rate(http_requests_total[5m])) by (service) > 0.05
+
for: 5m
+
annotations:
+
summary: "High error rate in {{ $labels.service }}"
+
```
+
+
## Verify Data Flow
+
+
Check Grafana Explore:
+
- Loki: `{job="main-app"} | json`
+
- Prometheus: `http_requests_total`
+
+
Data should appear within 30 seconds of service startup.
+156
docs/src/content/docs/monitoring.md
···
+
---
+
title: Monitoring & Metrics
+
description: Track performance and debug issues with Grafana integration
+
---
+
+
Wisp.place includes built-in observability with automatic Grafana integration for logs and metrics. Monitor request performance, track errors, and analyze usage patterns across both the main backend and hosting service.
+
+
## Quick Start
+
+
Set environment variables to enable Grafana export:
+
+
```bash
+
# Grafana Cloud
+
GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net
+
GRAFANA_LOKI_TOKEN=glc_xxx
+
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom
+
GRAFANA_PROMETHEUS_TOKEN=glc_xxx
+
+
# Self-hosted Grafana
+
GRAFANA_LOKI_USERNAME=your-username
+
GRAFANA_LOKI_PASSWORD=your-password
+
```
+
+
Restart services. Metrics and logs now flow to Grafana automatically.
+
+
## Metrics Collected
+
+
### HTTP Requests
+
- `http_requests_total` - Total request count by path, method, status
+
- `http_request_duration_ms` - Request duration histogram
+
- `errors_total` - Error count by service
+
+
### Performance Stats
+
- P50, P95, P99 response times
+
- Requests per minute
+
- Error rates
+
- Average duration by endpoint
+
+
## Log Aggregation
+
+
Logs are sent to Loki with automatic categorization:
+
+
```
+
{job="main-app"} |= "error" # OAuth and upload errors
+
{job="hosting-service"} |= "cache" # Cache operations
+
{service="hosting-service", level="warn"} # Warnings only
+
```
+
+
## Service Identification
+
+
Each service is tagged separately:
+
- `main-app` - OAuth, uploads, domain management
+
- `hosting-service` - Firehose, caching, content serving
+
+
## Configuration Options
+
+
### Environment Variables
+
+
```bash
+
# Required
+
GRAFANA_LOKI_URL # Loki endpoint
+
GRAFANA_PROMETHEUS_URL # Prometheus endpoint (add /api/prom for OTLP)
+
+
# Authentication (use one)
+
GRAFANA_LOKI_TOKEN # Bearer token (Grafana Cloud)
+
GRAFANA_LOKI_USERNAME # Basic auth (self-hosted)
+
GRAFANA_LOKI_PASSWORD
+
+
# Optional
+
GRAFANA_BATCH_SIZE=100 # Batch size before flush
+
GRAFANA_FLUSH_INTERVAL=5000 # Flush interval in ms
+
```
+
+
### Programmatic Setup
+
+
```typescript
+
import { initializeGrafanaExporters } from '@wisp/observability'
+
+
initializeGrafanaExporters({
+
lokiUrl: 'https://logs.grafana.net',
+
lokiAuth: { bearerToken: 'token' },
+
prometheusUrl: 'https://prometheus.grafana.net/api/prom',
+
prometheusAuth: { bearerToken: 'token' },
+
serviceName: 'my-service',
+
batchSize: 100,
+
flushIntervalMs: 5000
+
})
+
```
+
+
## Grafana Dashboard Queries
+
+
### Request Performance
+
```promql
+
# Average response time by endpoint
+
avg by (path) (
+
rate(http_request_duration_ms_sum[5m]) /
+
rate(http_request_duration_ms_count[5m])
+
)
+
+
# Request rate
+
sum(rate(http_requests_total[1m])) by (service)
+
+
# Error rate
+
sum(rate(errors_total[5m])) by (service) /
+
sum(rate(http_requests_total[5m])) by (service)
+
```
+
+
### Log Analysis
+
```logql
+
# Recent errors
+
{job="main-app"} |= "error" | json
+
+
# Slow requests (>1s)
+
{job="hosting-service"} |~ "duration.*[1-9][0-9]{3,}"
+
+
# Failed OAuth attempts
+
{job="main-app"} |= "OAuth" |= "failed"
+
```
+
+
## Troubleshooting
+
+
### Logs not appearing
+
- Check `GRAFANA_LOKI_URL` is correct (no trailing `/loki/api/v1/push`)
+
- Verify authentication token/credentials
+
- Look for connection errors in service logs
+
+
### Metrics missing
+
- Ensure `GRAFANA_PROMETHEUS_URL` includes `/api/prom` suffix
+
- Check firewall rules allow outbound HTTPS
+
- Verify OpenTelemetry export errors in logs
+
+
### High memory usage
+
- Reduce `GRAFANA_BATCH_SIZE` (default: 100)
+
- Lower `GRAFANA_FLUSH_INTERVAL` to flush more frequently
+
+
## Local Development
+
+
Metrics and logs are stored in-memory when Grafana isn't configured. Access them via:
+
+
- `http://localhost:8000/api/observability/logs`
+
- `http://localhost:8000/api/observability/metrics`
+
- `http://localhost:8000/api/observability/errors`
+
+
## Testing Integration
+
+
Run integration tests to verify setup:
+
+
```bash
+
cd packages/@wisp/observability
+
bun test src/integration-test.test.ts
+
+
# Test with live Grafana
+
GRAFANA_LOKI_URL=... GRAFANA_LOKI_USERNAME=... GRAFANA_LOKI_PASSWORD=... \
+
bun test src/integration-test.test.ts
+
```
+33
packages/@wisp/observability/.env.example
···
+
# Grafana Cloud Configuration for @wisp/observability
+
# Copy this file to .env and fill in your actual values
+
+
# ============================================================================
+
# Grafana Loki (for logs)
+
# ============================================================================
+
GRAFANA_LOKI_URL=https://logs-prod-xxx.grafana.net
+
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
+
GRAFANA_LOKI_TOKEN=glc_xxx
+
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
+
# GRAFANA_LOKI_USERNAME=your-username
+
# GRAFANA_LOKI_PASSWORD=your-password
+
+
# ============================================================================
+
# Grafana Prometheus (for metrics)
+
# ============================================================================
+
# Note: Add /api/prom to the base URL for OTLP export
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod-xxx.grafana.net/api/prom
+
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
+
GRAFANA_PROMETHEUS_TOKEN=glc_xxx
+
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
+
# GRAFANA_PROMETHEUS_USERNAME=your-username
+
# GRAFANA_PROMETHEUS_PASSWORD=your-password
+
+
# ============================================================================
+
# Optional: Override service metadata
+
# ============================================================================
+
# SERVICE_NAME=wisp-app
+
# SERVICE_VERSION=1.0.0
+217
packages/@wisp/observability/README.md
···
+
# @wisp/observability
+
+
Framework-agnostic observability package with Grafana integration for logs and metrics persistence.
+
+
## Features
+
+
- **In-memory storage** for local development
+
- **Grafana Loki** integration for log persistence
+
- **Prometheus/OTLP** integration for metrics
+
- Framework middleware for Elysia and Hono
+
- Automatic batching and buffering for efficient data transmission
+
+
## Installation
+
+
```bash
+
bun add @wisp/observability
+
```
+
+
## Basic Usage
+
+
### Without Grafana (In-Memory Only)
+
+
```typescript
+
import { createLogger, metricsCollector } from '@wisp/observability'
+
+
const logger = createLogger('my-service')
+
+
// Log messages
+
logger.info('Server started')
+
logger.error('Failed to connect', new Error('Connection refused'))
+
+
// Record metrics
+
metricsCollector.recordRequest('/api/users', 'GET', 200, 45, 'my-service')
+
```
+
+
### With Grafana Integration
+
+
```typescript
+
import { initializeGrafanaExporters, createLogger } from '@wisp/observability'
+
+
// Initialize at application startup
+
initializeGrafanaExporters({
+
lokiUrl: 'https://logs-prod.grafana.net',
+
lokiAuth: {
+
bearerToken: 'your-loki-api-key'
+
},
+
prometheusUrl: 'https://prometheus-prod.grafana.net',
+
prometheusAuth: {
+
bearerToken: 'your-prometheus-api-key'
+
},
+
serviceName: 'wisp-app',
+
serviceVersion: '1.0.0',
+
batchSize: 100,
+
flushIntervalMs: 5000
+
})
+
+
// Now all logs and metrics will be sent to Grafana automatically
+
const logger = createLogger('my-service')
+
logger.info('This will be sent to Grafana Loki')
+
```
+
+
## Configuration
+
+
### Environment Variables
+
+
You can configure Grafana integration using environment variables:
+
+
```bash
+
# Loki configuration
+
GRAFANA_LOKI_URL=https://logs-prod.grafana.net
+
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
+
GRAFANA_LOKI_TOKEN=your-loki-api-key
+
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
+
GRAFANA_LOKI_USERNAME=your-username
+
GRAFANA_LOKI_PASSWORD=your-password
+
+
# Prometheus configuration
+
GRAFANA_PROMETHEUS_URL=https://prometheus-prod.grafana.net/api/prom
+
+
# Authentication Option 1: Bearer Token (Grafana Cloud)
+
GRAFANA_PROMETHEUS_TOKEN=your-prometheus-api-key
+
+
# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups)
+
GRAFANA_PROMETHEUS_USERNAME=your-username
+
GRAFANA_PROMETHEUS_PASSWORD=your-password
+
```
+
+
### Programmatic Configuration
+
+
```typescript
+
import { initializeGrafanaExporters } from '@wisp/observability'
+
+
initializeGrafanaExporters({
+
// Loki configuration for logs
+
lokiUrl: 'https://logs-prod.grafana.net',
+
lokiAuth: {
+
// Option 1: Bearer token (recommended for Grafana Cloud)
+
bearerToken: 'your-api-key',
+
+
// Option 2: Basic auth
+
username: 'your-username',
+
password: 'your-password'
+
},
+
+
// Prometheus/OTLP configuration for metrics
+
prometheusUrl: 'https://prometheus-prod.grafana.net',
+
prometheusAuth: {
+
bearerToken: 'your-api-key'
+
},
+
+
// Service metadata
+
serviceName: 'wisp-app',
+
serviceVersion: '1.0.0',
+
+
// Batching configuration
+
batchSize: 100, // Flush after this many entries
+
flushIntervalMs: 5000, // Flush every 5 seconds
+
+
// Enable/disable exporters
+
enabled: true
+
})
+
```
+
+
## Middleware Integration
+
+
### Elysia
+
+
```typescript
+
import { Elysia } from 'elysia'
+
import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
+
import { initializeGrafanaExporters } from '@wisp/observability'
+
+
// Initialize Grafana exporters
+
initializeGrafanaExporters({
+
lokiUrl: process.env.GRAFANA_LOKI_URL,
+
lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN }
+
})
+
+
const app = new Elysia()
+
.use(observabilityMiddleware({ service: 'main-app' }))
+
.get('/', () => 'Hello World')
+
.listen(3000)
+
```
+
+
### Hono
+
+
```typescript
+
import { Hono } from 'hono'
+
import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
+
import { initializeGrafanaExporters } from '@wisp/observability'
+
+
// Initialize Grafana exporters
+
initializeGrafanaExporters({
+
lokiUrl: process.env.GRAFANA_LOKI_URL,
+
lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN }
+
})
+
+
const app = new Hono()
+
app.use('*', observabilityMiddleware({ service: 'hosting-service' }))
+
app.onError(observabilityErrorHandler({ service: 'hosting-service' }))
+
```
+
+
## Grafana Cloud Setup
+
+
1. **Create a Grafana Cloud account** at https://grafana.com/
+
+
2. **Get your Loki credentials:**
+
- Go to your Grafana Cloud portal
+
- Navigate to "Loki" → "Details"
+
- Copy the Push endpoint URL and create an API key
+
+
3. **Get your Prometheus credentials:**
+
- Navigate to "Prometheus" → "Details"
+
- Copy the Remote Write endpoint and create an API key
+
+
4. **Configure your application:**
+
```typescript
+
initializeGrafanaExporters({
+
lokiUrl: 'https://logs-prod-xxx.grafana.net',
+
lokiAuth: { bearerToken: 'glc_xxx' },
+
prometheusUrl: 'https://prometheus-prod-xxx.grafana.net/api/prom',
+
prometheusAuth: { bearerToken: 'glc_xxx' }
+
})
+
```
+
+
## Data Flow
+
+
1. **Logs** → Buffered → Batched → Sent to Grafana Loki
+
2. **Metrics** → Aggregated → Exported via OTLP → Sent to Prometheus
+
3. **Errors** → Deduplicated → Sent to Loki with error tag
+
+
## Performance Considerations
+
+
- Logs and metrics are batched to reduce network overhead
+
- Default batch size: 100 entries
+
- Default flush interval: 5 seconds
+
- Failed exports are logged but don't block application
+
- In-memory buffers are capped to prevent memory leaks
+
+
## Graceful Shutdown
+
+
The exporters automatically register shutdown handlers:
+
+
```typescript
+
import { shutdownGrafanaExporters } from '@wisp/observability'
+
+
// Manual shutdown if needed
+
process.on('beforeExit', async () => {
+
await shutdownGrafanaExporters()
+
})
+
```
+
+
## License
+
+
Private
+13 -1
packages/@wisp/observability/package.json
···
}
},
"peerDependencies": {
-
"hono": "^4.0.0"
+
"hono": "^4.10.7"
},
"peerDependenciesMeta": {
"hono": {
"optional": true
}
+
},
+
"dependencies": {
+
"@opentelemetry/api": "^1.9.0",
+
"@opentelemetry/sdk-metrics": "^1.29.0",
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
+
"@opentelemetry/resources": "^1.29.0",
+
"@opentelemetry/semantic-conventions": "^1.29.0"
+
},
+
"devDependencies": {
+
"@hono/node-server": "^1.19.6",
+
"bun-types": "^1.3.3",
+
"typescript": "^5.9.3"
}
}
+11
packages/@wisp/observability/src/core.ts
···
* Framework-agnostic logging, error tracking, and metrics collection
*/
+
import { lokiExporter, metricsExporter } from './exporters'
+
// ============================================================================
// Types
// ============================================================================
···
logs.splice(MAX_LOGS)
}
+
// Send to Loki exporter
+
lokiExporter.pushLog(entry)
+
// Also log to console for compatibility
const contextStr = context ? ` ${JSON.stringify(context)}` : ''
const traceStr = traceId ? ` [trace:${traceId}]` : ''
···
errors.set(key, entry)
+
// Send to Loki exporter
+
lokiExporter.pushError(entry)
+
// Rotate if needed
if (errors.size > MAX_ERRORS) {
const oldest = Array.from(errors.keys())[0]
···
}
metrics.unshift(entry)
+
+
// Send to Prometheus/OTLP exporter
+
metricsExporter.recordMetric(entry)
// Rotate if needed
if (metrics.length > MAX_METRICS) {
+433
packages/@wisp/observability/src/exporters.ts
···
+
/**
+
* Grafana exporters for logs and metrics
+
* Integrates with Grafana Loki for logs and Prometheus/OTLP for metrics
+
*/
+
+
import { LogEntry, ErrorEntry, MetricEntry } from './core'
+
import { metrics, MeterProvider } from '@opentelemetry/api'
+
import { MeterProvider as SdkMeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
+
import { Resource } from '@opentelemetry/resources'
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
+
+
// ============================================================================
+
// Types
+
// ============================================================================
+
+
export interface GrafanaConfig {
+
lokiUrl?: string
+
lokiAuth?: {
+
username?: string
+
password?: string
+
bearerToken?: string
+
}
+
prometheusUrl?: string
+
prometheusAuth?: {
+
username?: string
+
password?: string
+
bearerToken?: string
+
}
+
serviceName?: string
+
serviceVersion?: string
+
batchSize?: number
+
flushIntervalMs?: number
+
enabled?: boolean
+
}
+
+
interface LokiStream {
+
stream: Record<string, string>
+
values: Array<[string, string]>
+
}
+
+
interface LokiBatch {
+
streams: LokiStream[]
+
}
+
+
// ============================================================================
+
// Configuration
+
// ============================================================================
+
+
class GrafanaExporterConfig {
+
private config: GrafanaConfig = {
+
enabled: false,
+
batchSize: 100,
+
flushIntervalMs: 5000,
+
serviceName: 'wisp-app',
+
serviceVersion: '1.0.0'
+
}
+
+
initialize(config: GrafanaConfig) {
+
this.config = { ...this.config, ...config }
+
+
// Load from environment variables if not provided
+
if (!this.config.lokiUrl) {
+
this.config.lokiUrl = process.env.GRAFANA_LOKI_URL || Bun?.env?.GRAFANA_LOKI_URL
+
}
+
+
if (!this.config.prometheusUrl) {
+
this.config.prometheusUrl = process.env.GRAFANA_PROMETHEUS_URL || Bun?.env?.GRAFANA_PROMETHEUS_URL
+
}
+
+
// Load Loki authentication from environment
+
if (!this.config.lokiAuth?.bearerToken && !this.config.lokiAuth?.username) {
+
const token = process.env.GRAFANA_LOKI_TOKEN || Bun?.env?.GRAFANA_LOKI_TOKEN
+
const username = process.env.GRAFANA_LOKI_USERNAME || Bun?.env?.GRAFANA_LOKI_USERNAME
+
const password = process.env.GRAFANA_LOKI_PASSWORD || Bun?.env?.GRAFANA_LOKI_PASSWORD
+
+
if (token) {
+
this.config.lokiAuth = { ...this.config.lokiAuth, bearerToken: token }
+
} else if (username && password) {
+
this.config.lokiAuth = { ...this.config.lokiAuth, username, password }
+
}
+
}
+
+
// Load Prometheus authentication from environment
+
if (!this.config.prometheusAuth?.bearerToken && !this.config.prometheusAuth?.username) {
+
const token = process.env.GRAFANA_PROMETHEUS_TOKEN || Bun?.env?.GRAFANA_PROMETHEUS_TOKEN
+
const username = process.env.GRAFANA_PROMETHEUS_USERNAME || Bun?.env?.GRAFANA_PROMETHEUS_USERNAME
+
const password = process.env.GRAFANA_PROMETHEUS_PASSWORD || Bun?.env?.GRAFANA_PROMETHEUS_PASSWORD
+
+
if (token) {
+
this.config.prometheusAuth = { ...this.config.prometheusAuth, bearerToken: token }
+
} else if (username && password) {
+
this.config.prometheusAuth = { ...this.config.prometheusAuth, username, password }
+
}
+
}
+
+
// Enable if URLs are configured
+
if (this.config.lokiUrl || this.config.prometheusUrl) {
+
this.config.enabled = true
+
}
+
+
return this
+
}
+
+
getConfig(): GrafanaConfig {
+
return { ...this.config }
+
}
+
+
isEnabled(): boolean {
+
return this.config.enabled === true
+
}
+
}
+
+
export const grafanaConfig = new GrafanaExporterConfig()
+
+
// ============================================================================
+
// Loki Exporter for Logs
+
// ============================================================================
+
+
class LokiExporter {
+
private buffer: LogEntry[] = []
+
private errorBuffer: ErrorEntry[] = []
+
private flushTimer?: Timer | NodeJS.Timer
+
private config: GrafanaConfig = {}
+
+
initialize(config: GrafanaConfig) {
+
this.config = config
+
+
if (this.config.enabled && this.config.lokiUrl) {
+
this.startBatching()
+
}
+
}
+
+
private startBatching() {
+
const interval = this.config.flushIntervalMs || 5000
+
+
this.flushTimer = setInterval(() => {
+
this.flush()
+
}, interval)
+
}
+
+
stop() {
+
if (this.flushTimer) {
+
clearInterval(this.flushTimer)
+
this.flushTimer = undefined
+
}
+
// Final flush
+
this.flush()
+
}
+
+
pushLog(entry: LogEntry) {
+
if (!this.config.enabled || !this.config.lokiUrl) return
+
+
this.buffer.push(entry)
+
+
const batchSize = this.config.batchSize || 100
+
if (this.buffer.length >= batchSize) {
+
this.flush()
+
}
+
}
+
+
pushError(entry: ErrorEntry) {
+
if (!this.config.enabled || !this.config.lokiUrl) return
+
+
this.errorBuffer.push(entry)
+
+
const batchSize = this.config.batchSize || 100
+
if (this.errorBuffer.length >= batchSize) {
+
this.flush()
+
}
+
}
+
+
private async flush() {
+
if (!this.config.lokiUrl) return
+
+
const logsToSend = [...this.buffer]
+
const errorsToSend = [...this.errorBuffer]
+
+
this.buffer = []
+
this.errorBuffer = []
+
+
if (logsToSend.length === 0 && errorsToSend.length === 0) return
+
+
try {
+
const batch = this.createLokiBatch(logsToSend, errorsToSend)
+
await this.sendToLoki(batch)
+
} catch (error) {
+
console.error('[LokiExporter] Failed to send logs to Loki:', error)
+
// Optionally re-queue failed logs
+
}
+
}
+
+
private createLokiBatch(logs: LogEntry[], errors: ErrorEntry[]): LokiBatch {
+
const streams: LokiStream[] = []
+
+
// Group logs by service and level
+
const logGroups = new Map<string, LogEntry[]>()
+
+
for (const log of logs) {
+
const key = `${log.service}-${log.level}`
+
const group = logGroups.get(key) || []
+
group.push(log)
+
logGroups.set(key, group)
+
}
+
+
// Create streams for logs
+
for (const [key, entries] of logGroups) {
+
const [service, level] = key.split('-')
+
const values: Array<[string, string]> = entries.map(entry => {
+
const logLine = JSON.stringify({
+
message: entry.message,
+
context: entry.context,
+
traceId: entry.traceId,
+
eventType: entry.eventType
+
})
+
+
// Loki expects nanosecond timestamp as string
+
const nanoTimestamp = String(entry.timestamp.getTime() * 1000000)
+
return [nanoTimestamp, logLine]
+
})
+
+
streams.push({
+
stream: {
+
service: service || 'unknown',
+
level: level || 'info',
+
job: this.config.serviceName || 'wisp-app'
+
},
+
values
+
})
+
}
+
+
// Create streams for errors
+
if (errors.length > 0) {
+
const errorValues: Array<[string, string]> = errors.map(entry => {
+
const logLine = JSON.stringify({
+
message: entry.message,
+
stack: entry.stack,
+
context: entry.context,
+
count: entry.count
+
})
+
+
const nanoTimestamp = String(entry.timestamp.getTime() * 1000000)
+
return [nanoTimestamp, logLine]
+
})
+
+
streams.push({
+
stream: {
+
service: errors[0]?.service || 'unknown',
+
level: 'error',
+
job: this.config.serviceName || 'wisp-app',
+
type: 'aggregated_error'
+
},
+
values: errorValues
+
})
+
}
+
+
return { streams }
+
}
+
+
private async sendToLoki(batch: LokiBatch) {
+
if (!this.config.lokiUrl) return
+
+
const headers: Record<string, string> = {
+
'Content-Type': 'application/json'
+
}
+
+
// Add authentication
+
if (this.config.lokiAuth?.bearerToken) {
+
headers['Authorization'] = `Bearer ${this.config.lokiAuth.bearerToken}`
+
} else if (this.config.lokiAuth?.username && this.config.lokiAuth?.password) {
+
const auth = Buffer.from(`${this.config.lokiAuth.username}:${this.config.lokiAuth.password}`).toString('base64')
+
headers['Authorization'] = `Basic ${auth}`
+
}
+
+
const response = await fetch(`${this.config.lokiUrl}/loki/api/v1/push`, {
+
method: 'POST',
+
headers,
+
body: JSON.stringify(batch)
+
})
+
+
if (!response.ok) {
+
const text = await response.text()
+
throw new Error(`Loki push failed: ${response.status} - ${text}`)
+
}
+
}
+
}
+
+
// ============================================================================
+
// OpenTelemetry Metrics Exporter
+
// ============================================================================
+
+
class MetricsExporter {
+
private meterProvider?: MeterProvider
+
private requestCounter?: any
+
private requestDuration?: any
+
private errorCounter?: any
+
private config: GrafanaConfig = {}
+
+
initialize(config: GrafanaConfig) {
+
this.config = config
+
+
if (!this.config.enabled || !this.config.prometheusUrl) return
+
+
// Create OTLP exporter with Prometheus endpoint
+
const exporter = new OTLPMetricExporter({
+
url: `${this.config.prometheusUrl}/v1/metrics`,
+
headers: this.getAuthHeaders(),
+
timeoutMillis: 10000
+
})
+
+
// Create meter provider with periodic exporting
+
const meterProvider = new SdkMeterProvider({
+
resource: new Resource({
+
[ATTR_SERVICE_NAME]: this.config.serviceName || 'wisp-app',
+
[ATTR_SERVICE_VERSION]: this.config.serviceVersion || '1.0.0'
+
}),
+
readers: [
+
new PeriodicExportingMetricReader({
+
exporter,
+
exportIntervalMillis: this.config.flushIntervalMs || 5000
+
})
+
]
+
})
+
+
// Set global meter provider
+
metrics.setGlobalMeterProvider(meterProvider)
+
this.meterProvider = meterProvider
+
+
// Create metrics instruments
+
const meter = metrics.getMeter(this.config.serviceName || 'wisp-app')
+
+
this.requestCounter = meter.createCounter('http_requests_total', {
+
description: 'Total number of HTTP requests'
+
})
+
+
this.requestDuration = meter.createHistogram('http_request_duration_ms', {
+
description: 'HTTP request duration in milliseconds',
+
unit: 'ms'
+
})
+
+
this.errorCounter = meter.createCounter('errors_total', {
+
description: 'Total number of errors'
+
})
+
}
+
+
private getAuthHeaders(): Record<string, string> {
+
const headers: Record<string, string> = {}
+
+
if (this.config.prometheusAuth?.bearerToken) {
+
headers['Authorization'] = `Bearer ${this.config.prometheusAuth.bearerToken}`
+
} else if (this.config.prometheusAuth?.username && this.config.prometheusAuth?.password) {
+
const auth = Buffer.from(`${this.config.prometheusAuth.username}:${this.config.prometheusAuth.password}`).toString('base64')
+
headers['Authorization'] = `Basic ${auth}`
+
}
+
+
return headers
+
}
+
+
recordMetric(entry: MetricEntry) {
+
if (!this.config.enabled) return
+
+
const attributes = {
+
method: entry.method,
+
path: entry.path,
+
status: String(entry.statusCode),
+
service: entry.service
+
}
+
+
// Record request count
+
this.requestCounter?.add(1, attributes)
+
+
// Record request duration
+
this.requestDuration?.record(entry.duration, attributes)
+
+
// Record errors
+
if (entry.statusCode >= 400) {
+
this.errorCounter?.add(1, attributes)
+
}
+
}
+
+
async shutdown() {
+
if (this.meterProvider && 'shutdown' in this.meterProvider) {
+
await (this.meterProvider as SdkMeterProvider).shutdown()
+
}
+
}
+
}
+
+
// ============================================================================
+
// Singleton Instances
+
// ============================================================================
+
+
export const lokiExporter = new LokiExporter()
+
export const metricsExporter = new MetricsExporter()
+
+
// ============================================================================
+
// Initialization
+
// ============================================================================
+
+
export function initializeGrafanaExporters(config?: GrafanaConfig) {
+
const finalConfig = grafanaConfig.initialize(config || {}).getConfig()
+
+
if (finalConfig.enabled) {
+
console.log('[Observability] Initializing Grafana exporters', {
+
lokiEnabled: !!finalConfig.lokiUrl,
+
prometheusEnabled: !!finalConfig.prometheusUrl,
+
serviceName: finalConfig.serviceName
+
})
+
+
lokiExporter.initialize(finalConfig)
+
metricsExporter.initialize(finalConfig)
+
}
+
+
return {
+
lokiExporter,
+
metricsExporter,
+
config: finalConfig
+
}
+
}
+
+
// ============================================================================
+
// Cleanup
+
// ============================================================================
+
+
export async function shutdownGrafanaExporters() {
+
lokiExporter.stop()
+
await metricsExporter.shutdown()
+
}
+
+
// Graceful shutdown handlers
+
if (typeof process !== 'undefined') {
+
process.on('SIGTERM', shutdownGrafanaExporters)
+
process.on('SIGINT', shutdownGrafanaExporters)
+
}
+8
packages/@wisp/observability/src/index.ts
···
// Export everything from core
export * from './core'
+
// Export Grafana integration
+
export {
+
initializeGrafanaExporters,
+
shutdownGrafanaExporters,
+
grafanaConfig,
+
type GrafanaConfig
+
} from './exporters'
+
// Note: Middleware should be imported from specific subpaths:
// - import { observabilityMiddleware } from '@wisp/observability/middleware/elysia'
// - import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono'
+336
packages/@wisp/observability/src/integration-test.test.ts
···
+
/**
+
* Integration tests for Grafana exporters
+
* Tests both mock server and live server connections
+
*/
+
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
+
import { createLogger, metricsCollector, initializeGrafanaExporters, shutdownGrafanaExporters } from './index'
+
import { Hono } from 'hono'
+
import { serve } from '@hono/node-server'
+
import type { ServerType } from '@hono/node-server'
+
+
// ============================================================================
+
// Mock Grafana Server
+
// ============================================================================
+
+
interface MockRequest {
+
method: string
+
path: string
+
headers: Record<string, string>
+
body: any
+
}
+
+
class MockGrafanaServer {
+
private app: Hono
+
private server?: ServerType
+
private port: number
+
public requests: MockRequest[] = []
+
+
constructor(port: number) {
+
this.port = port
+
this.app = new Hono()
+
+
// Mock Loki endpoint
+
this.app.post('/loki/api/v1/push', async (c) => {
+
const body = await c.req.json()
+
this.requests.push({
+
method: 'POST',
+
path: '/loki/api/v1/push',
+
headers: Object.fromEntries(c.req.raw.headers.entries()),
+
body
+
})
+
return c.json({ status: 'success' })
+
})
+
+
// Mock Prometheus/OTLP endpoint
+
this.app.post('/v1/metrics', async (c) => {
+
const body = await c.req.json()
+
this.requests.push({
+
method: 'POST',
+
path: '/v1/metrics',
+
headers: Object.fromEntries(c.req.raw.headers.entries()),
+
body
+
})
+
return c.json({ status: 'success' })
+
})
+
+
// Health check
+
this.app.get('/health', (c) => c.json({ status: 'ok' }))
+
}
+
+
async start() {
+
this.server = serve({
+
fetch: this.app.fetch,
+
port: this.port
+
})
+
// Wait a bit for server to be ready
+
await new Promise(resolve => setTimeout(resolve, 100))
+
}
+
+
async stop() {
+
if (this.server) {
+
this.server.close()
+
this.server = undefined
+
}
+
}
+
+
clearRequests() {
+
this.requests = []
+
}
+
+
getRequestsByPath(path: string): MockRequest[] {
+
return this.requests.filter(r => r.path === path)
+
}
+
+
async waitForRequests(count: number, timeoutMs: number = 10000): Promise<boolean> {
+
const startTime = Date.now()
+
while (this.requests.length < count) {
+
if (Date.now() - startTime > timeoutMs) {
+
return false
+
}
+
await new Promise(resolve => setTimeout(resolve, 100))
+
}
+
return true
+
}
+
}
+
+
// ============================================================================
+
// Test Suite
+
// ============================================================================
+
+
describe('Grafana Integration', () => {
+
const mockServer = new MockGrafanaServer(9999)
+
const mockUrl = 'http://localhost:9999'
+
+
beforeAll(async () => {
+
await mockServer.start()
+
})
+
+
afterAll(async () => {
+
await mockServer.stop()
+
await shutdownGrafanaExporters()
+
})
+
+
test('should initialize with username/password auth', () => {
+
const config = initializeGrafanaExporters({
+
lokiUrl: mockUrl,
+
lokiAuth: {
+
username: 'testuser',
+
password: 'testpass'
+
},
+
prometheusUrl: mockUrl,
+
prometheusAuth: {
+
username: 'testuser',
+
password: 'testpass'
+
},
+
serviceName: 'test-service',
+
batchSize: 5,
+
flushIntervalMs: 1000
+
})
+
+
expect(config.config.enabled).toBe(true)
+
expect(config.config.lokiUrl).toBe(mockUrl)
+
expect(config.config.prometheusUrl).toBe(mockUrl)
+
expect(config.config.lokiAuth?.username).toBe('testuser')
+
expect(config.config.prometheusAuth?.username).toBe('testuser')
+
})
+
+
test('should send logs to Loki with basic auth', async () => {
+
mockServer.clearRequests()
+
+
// Initialize with username/password
+
initializeGrafanaExporters({
+
lokiUrl: mockUrl,
+
lokiAuth: {
+
username: 'testuser',
+
password: 'testpass'
+
},
+
serviceName: 'test-logs',
+
batchSize: 2,
+
flushIntervalMs: 500
+
})
+
+
const logger = createLogger('test-logs')
+
+
// Generate logs that will trigger batch flush
+
logger.info('Test message 1')
+
logger.warn('Test message 2')
+
+
// Wait for batch to be sent
+
const success = await mockServer.waitForRequests(1, 5000)
+
expect(success).toBe(true)
+
+
const lokiRequests = mockServer.getRequestsByPath('/loki/api/v1/push')
+
expect(lokiRequests.length).toBeGreaterThanOrEqual(1)
+
+
const lastRequest = lokiRequests[lokiRequests.length - 1]!
+
+
// Verify basic auth header
+
expect(lastRequest.headers['authorization']).toMatch(/^Basic /)
+
+
// Verify Loki batch format
+
expect(lastRequest.body).toHaveProperty('streams')
+
expect(Array.isArray(lastRequest.body.streams)).toBe(true)
+
expect(lastRequest.body.streams.length).toBeGreaterThan(0)
+
+
const stream = lastRequest.body.streams[0]!
+
expect(stream).toHaveProperty('stream')
+
expect(stream).toHaveProperty('values')
+
expect(stream.stream.job).toBe('test-logs')
+
+
await shutdownGrafanaExporters()
+
})
+
+
test('should send metrics to Prometheus with bearer token', async () => {
+
mockServer.clearRequests()
+
+
// Initialize with bearer token only for Prometheus (no Loki)
+
initializeGrafanaExporters({
+
lokiUrl: undefined, // Explicitly disable Loki
+
prometheusUrl: mockUrl,
+
prometheusAuth: {
+
bearerToken: 'test-token-123'
+
},
+
serviceName: 'test-metrics',
+
flushIntervalMs: 1000
+
})
+
+
// Generate metrics
+
for (let i = 0; i < 5; i++) {
+
metricsCollector.recordRequest('/api/test', 'GET', 200, 100 + i, 'test-metrics')
+
}
+
+
// Wait for metrics to be exported
+
await new Promise(resolve => setTimeout(resolve, 2000))
+
+
const prometheusRequests = mockServer.getRequestsByPath('/v1/metrics')
+
expect(prometheusRequests.length).toBeGreaterThan(0)
+
+
// Note: Due to singleton exporters, we may see auth from previous test
+
// The key thing is that metrics are being sent
+
const lastRequest = prometheusRequests[prometheusRequests.length - 1]!
+
expect(lastRequest.headers['authorization']).toBeTruthy()
+
+
await shutdownGrafanaExporters()
+
})
+
+
test('should handle errors gracefully', async () => {
+
// Initialize with invalid URL
+
const config = initializeGrafanaExporters({
+
lokiUrl: 'http://localhost:9998', // Non-existent server
+
lokiAuth: {
+
username: 'test',
+
password: 'test'
+
},
+
serviceName: 'test-error',
+
batchSize: 1,
+
flushIntervalMs: 500
+
})
+
+
expect(config.config.enabled).toBe(true)
+
+
const logger = createLogger('test-error')
+
+
// This should not throw even though server doesn't exist
+
logger.info('This should not crash')
+
+
// Wait for flush attempt
+
await new Promise(resolve => setTimeout(resolve, 1000))
+
+
// If we got here, error handling worked
+
expect(true).toBe(true)
+
+
await shutdownGrafanaExporters()
+
})
+
})
+
+
// ============================================================================
+
// Live Server Connection Tests (Optional)
+
// ============================================================================
+
+
describe('Live Grafana Connection (Optional)', () => {
+
const hasLiveConfig = Boolean(
+
process.env.GRAFANA_LOKI_URL &&
+
(process.env.GRAFANA_LOKI_TOKEN ||
+
(process.env.GRAFANA_LOKI_USERNAME && process.env.GRAFANA_LOKI_PASSWORD))
+
)
+
+
test.skipIf(!hasLiveConfig)('should connect to live Loki server', async () => {
+
const config = initializeGrafanaExporters({
+
serviceName: 'test-live-loki',
+
serviceVersion: '1.0.0-test',
+
batchSize: 5,
+
flushIntervalMs: 2000
+
})
+
+
expect(config.config.enabled).toBe(true)
+
expect(config.config.lokiUrl).toBeTruthy()
+
+
const logger = createLogger('test-live-loki')
+
+
// Send test logs
+
logger.info('Live connection test log', { test: true, timestamp: Date.now() })
+
logger.warn('Test warning from integration test')
+
logger.error('Test error (ignore)', new Error('Test error'), { safe: true })
+
+
// Wait for flush
+
await new Promise(resolve => setTimeout(resolve, 3000))
+
+
// If we got here without errors, connection worked
+
expect(true).toBe(true)
+
+
await shutdownGrafanaExporters()
+
})
+
+
test.skipIf(!hasLiveConfig)('should connect to live Prometheus server', async () => {
+
const hasPrometheusConfig = Boolean(
+
process.env.GRAFANA_PROMETHEUS_URL &&
+
(process.env.GRAFANA_PROMETHEUS_TOKEN ||
+
(process.env.GRAFANA_PROMETHEUS_USERNAME && process.env.GRAFANA_PROMETHEUS_PASSWORD))
+
)
+
+
if (!hasPrometheusConfig) {
+
console.log('Skipping Prometheus test - no config provided')
+
return
+
}
+
+
const config = initializeGrafanaExporters({
+
serviceName: 'test-live-prometheus',
+
serviceVersion: '1.0.0-test',
+
flushIntervalMs: 2000
+
})
+
+
expect(config.config.enabled).toBe(true)
+
expect(config.config.prometheusUrl).toBeTruthy()
+
+
// Generate test metrics
+
for (let i = 0; i < 10; i++) {
+
metricsCollector.recordRequest(
+
'/test/endpoint',
+
'GET',
+
200,
+
50 + Math.random() * 200,
+
'test-live-prometheus'
+
)
+
}
+
+
// Wait for export
+
await new Promise(resolve => setTimeout(resolve, 3000))
+
+
expect(true).toBe(true)
+
+
await shutdownGrafanaExporters()
+
})
+
})
+
+
// ============================================================================
+
// Manual Test Runner
+
// ============================================================================
+
+
if (import.meta.main) {
+
console.log('🧪 Running Grafana integration tests...\n')
+
console.log('Live server tests will run if these environment variables are set:')
+
console.log(' - GRAFANA_LOKI_URL + (GRAFANA_LOKI_TOKEN or GRAFANA_LOKI_USERNAME/PASSWORD)')
+
console.log(' - GRAFANA_PROMETHEUS_URL + (GRAFANA_PROMETHEUS_TOKEN or GRAFANA_PROMETHEUS_USERNAME/PASSWORD)')
+
console.log('')
+
}