Openstatus www.openstatus.dev

chore: improve logging (#1718)

* try loggingsucks

* ci: apply automated fixes

* try loggingsucks

* ci: apply automated fixes

* trying loging sucks

* upgrade logtap

* ci: apply automated fixes

* fix test

* improve logging

* improve logging

* ci: apply automated fixes

* improve logging

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Thibault Le Ouay
autofix-ci[bot]
and committed by
GitHub
e17b6a6a 40278f32

+573 -136
+4 -1
apps/server/package.json
··· 15 15 "@hono/sentry": "1.2.2", 16 16 "@hono/zod-openapi": "1.1.5", 17 17 "@hono/zod-validator": "0.7.6", 18 - "@logtape/logtape": "1.1.2", 18 + "@logtape/logtape": "2.0.0", 19 + "@logtape/otel": "2.0.0", 19 20 "@logtape/sentry": "1.1.2", 20 21 "@openstatus/analytics": "workspace:*", 21 22 "@openstatus/assertions": "workspace:*", ··· 27 28 "@openstatus/tracker": "workspace:*", 28 29 "@openstatus/upstash": "workspace:*", 29 30 "@openstatus/utils": "workspace:*", 31 + "@opentelemetry/resources": "2.2.0", 32 + "@opentelemetry/semantic-conventions": "1.38.0", 30 33 "@scalar/hono-api-reference": "0.8.5", 31 34 "@t3-oss/env-core": "0.13.10", 32 35 "@unkey/api": "2.2.0",
+2
apps/server/src/env.ts
··· 16 16 NODE_ENV: z.string().prefault("development"), 17 17 SUPER_ADMIN_TOKEN: z.string(), 18 18 RESEND_API_KEY: z.string(), 19 + AXIOM_TOKEN: z.string(), 20 + AXIOM_DATASET: z.string(), 19 21 }, 20 22 21 23 /**
+98 -14
apps/server/src/index.ts
··· 2 2 3 3 import { sentry } from "@hono/sentry"; 4 4 import { 5 - configureSync, 5 + configure, 6 + // configureSync, 6 7 getConsoleSink, 7 8 getLogger, 8 9 jsonLinesFormatter, 9 10 withContext, 10 11 } from "@logtape/logtape"; 12 + import { getOpenTelemetrySink } from "@logtape/otel"; 11 13 import { Hono } from "hono"; 12 14 import { showRoutes } from "hono/dev"; 13 15 16 + import { resourceFromAttributes } from "@opentelemetry/resources"; 17 + import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from "@opentelemetry/semantic-conventions/incubating"; 14 18 import { prettyJSON } from "hono/pretty-json"; 15 19 import { requestId } from "hono/request-id"; 16 20 import { env } from "./env"; ··· 18 22 import { publicRoute } from "./routes/public"; 19 23 import { api } from "./routes/v1"; 20 24 21 - configureSync({ 25 + type Env = { 26 + Variables: { 27 + event: Record<string, unknown>; 28 + }; 29 + }; 30 + 31 + /* biome-ignore lint/suspicious/noExplicitAny: <explanation> */ 32 + function shouldSample(event: Record<string, any>): boolean { 33 + // Always keep errors 34 + if (event.status_code >= 500) return true; 35 + if (event.error) return true; 36 + 37 + // Always keep slow requests (above p99) 38 + if (event.duration_ms > 2000) return true; 39 + 40 + // Random sample the rest at 5% 41 + return Math.random() < 0.05; 42 + } 43 + 44 + const defaultLogger = getOpenTelemetrySink({ 45 + serviceName: "openstatus-server", 46 + otlpExporterConfig: { 47 + url: "https://eu-central-1.aws.edge.axiom.co/v1/logs", 48 + headers: { 49 + Authorization: `Bearer ${env.AXIOM_TOKEN}`, 50 + "X-Axiom-Dataset": env.AXIOM_DATASET, 51 + }, 52 + }, 53 + additionalResource: resourceFromAttributes({ 54 + [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: env.NODE_ENV, 55 + }), 56 + }); 57 + 58 + await configure({ 22 59 sinks: { 23 60 console: getConsoleSink({ formatter: jsonLinesFormatter }), 61 + 62 + otel: defaultLogger, 24 63 }, 25 64 loggers: [ 26 65 { 27 66 category: "api-server", 28 - lowestLevel: "debug", 67 + lowestLevel: "error", 29 68 sinks: ["console"], 30 69 }, 70 + { 71 + category: "api-server-otel", 72 + lowestLevel: "info", 73 + sinks: ["otel"], 74 + }, 31 75 ], 32 76 contextLocalStorage: new AsyncLocalStorage(), 33 77 }); 34 78 35 79 const logger = getLogger("api-server"); 36 80 37 - export const app = new Hono({ strict: false }); 81 + const otelLogger = getLogger("api-server-otel"); 82 + 83 + export const app = new Hono<Env>({ 84 + strict: false, 85 + }); 38 86 39 87 /** 40 88 * Middleware 41 89 */ 42 90 app.use("*", sentry({ dsn: process.env.SENTRY_DSN })); 43 91 app.use("*", requestId()); 44 - // app.use("*", logger()); 45 92 app.use("*", prettyJSON()); 46 93 47 94 app.use("*", async (c, next) => { ··· 54 101 method: c.req.method, 55 102 url: c.req.url, 56 103 userAgent: c.req.header("User-Agent"), 57 - // ipAddress: c.req.header("CF-Connecting-IP") || c.req.header("X-Forwarded-For") 58 104 }, 59 105 async () => { 60 - logger.info("Request started", { 106 + // Initialize wide event - one canonical log line per request 107 + const event: Record<string, unknown> = { 108 + timestamp: new Date().toISOString(), 109 + request_id: requestId, 110 + // Request context 61 111 method: c.req.method, 112 + path: c.req.path, 62 113 url: c.req.url, 63 - requestId, 64 - }); 114 + // Client context 115 + user_agent: c.req.header("User-Agent"), 116 + // Request metadata 117 + content_type: c.req.header("Content-Type"), 118 + }; 119 + c.set("event", event); 65 120 66 121 await next(); 67 122 123 + // Performance 68 124 const duration = Date.now() - startTime; 69 - logger.info("Request completed", { 70 - status: c.res.status, 71 - duration, 72 - requestId, 73 - }); 125 + event.duration_ms = duration; 126 + 127 + // Response context 128 + event.status_code = c.res.status; 129 + 130 + // Outcome 131 + if (c.error) { 132 + event.outcome = "error"; 133 + event.error = { 134 + type: c.error.name, 135 + message: c.error.message, 136 + stack: c.error.stack, 137 + }; 138 + } else { 139 + event.outcome = c.res.status < 400 ? "success" : "failure"; 140 + } 141 + 142 + // Emit single canonical log line (sampled for otel, always for console in dev) 143 + if (shouldSample(event)) { 144 + otelLogger.info("request", { ...event }); 145 + } 146 + 147 + // Console logging only for errors in production 148 + if (env.NODE_ENV !== "production" || c.res.status >= 500) { 149 + logger.info("request", { 150 + request_id: requestId, 151 + method: c.req.method, 152 + path: c.req.path, 153 + status_code: c.res.status, 154 + duration_ms: duration, 155 + outcome: event.outcome, 156 + }); 157 + } 74 158 }, 75 159 ); 76 160 });
+23 -12
apps/server/src/libs/middlewares/auth.ts
··· 14 14 verifyApiKeyHash, 15 15 } from "@openstatus/db/src/utils/api-key"; 16 16 17 - const logger = getLogger("api-server"); 17 + const logger = getLogger("api-server-otel"); 18 18 19 19 export async function authMiddleware( 20 20 c: Context<{ Variables: Variables }, "/*">, ··· 59 59 .get(); 60 60 61 61 if (!_workspace) { 62 - console.error("Workspace not found"); 62 + logger.error("Workspace not found for ownerId {ownerId}", { ownerId }); 63 63 throw new OpenStatusApiError({ 64 64 code: "NOT_FOUND", 65 65 message: "Workspace not found, please contact support", ··· 75 75 }); 76 76 } 77 77 78 + // Enrich wide event with business context 79 + const event = c.get("event"); 80 + event.workspace = { 81 + id: validation.data.id, 82 + name: validation.data.name, 83 + plan: validation.data.plan, 84 + stripe_id: validation.data.stripeId, 85 + }; 86 + event.auth_method = result.authMethod; 87 + 78 88 c.set("workspace", validation.data); 79 89 80 90 await next(); 81 91 } 82 92 83 93 async function validateKey(key: string): Promise<{ 84 - result: { valid: boolean; ownerId?: string }; 94 + result: { valid: boolean; ownerId?: string; authMethod?: string }; 85 95 error?: { message: string }; 86 96 }> { 87 97 if (env.NODE_ENV === "production") { ··· 130 140 .where(eq(apiKey.id, customKey.id)); 131 141 } 132 142 return { 133 - result: { valid: true, ownerId: String(customKey.workspaceId) }, 143 + result: { 144 + valid: true, 145 + ownerId: String(customKey.workspaceId), 146 + authMethod: "custom_key", 147 + }, 134 148 }; 135 149 } 136 150 ··· 144 158 error: { message: "Invalid API verification" }, 145 159 }; 146 160 } 147 - // Add deprecation header when Unkey key is used 148 - if (res.value.data.valid) { 149 - logger.info("Unkey key used - Workspace: {workspaceId}", { 150 - workspace: res.value.data.identity?.externalId, 151 - }); 152 - } 153 161 return { 154 162 result: { 155 163 valid: res.value.data.valid, 156 164 ownerId: res.value.data.identity?.externalId, 165 + authMethod: "unkey", 157 166 }, 158 167 error: undefined, 159 168 }; 160 169 } 161 170 // Special bypass for our workspace 162 171 if (key.startsWith("sa_") && key === env.SUPER_ADMIN_TOKEN) { 163 - return { result: { valid: true, ownerId: "1" } }; 172 + return { 173 + result: { valid: true, ownerId: "1", authMethod: "super_admin" }, 174 + }; 164 175 } 165 176 // In production, we only accept Unkey keys 166 177 throw new OpenStatusApiError({ ··· 170 181 } 171 182 172 183 // In dev / test mode we can use the key as the ownerId 173 - return { result: { valid: true, ownerId: key } }; 184 + return { result: { valid: true, ownerId: key, authMethod: "dev" } }; 174 185 }
+1
apps/server/src/types/index.ts
··· 3 3 4 4 export type Variables = RequestIdVariables & { 5 5 workspace: Workspace; 6 + event: Record<string, unknown>; 6 7 };
+2 -1
apps/server/tsconfig.json
··· 3 3 "include": ["src", "*.ts", "**/*.ts"], 4 4 "compilerOptions": { 5 5 "jsx": "react-jsx", 6 - "module": "preserve", 6 + "module": "ESNext", 7 + "target": "ESNext", 7 8 "moduleResolution": "bundler", 8 9 "jsxImportSource": "react", 9 10 "allowJs": true,
+5 -1
apps/workflows/.env.test
··· 13 13 SCREENSHOT_SERVICE_URL=http://your.endpoint 14 14 NEXT_PUBLIC_OPENPANEL_CLIENT_ID=test 15 15 OPENPANEL_CLIENT_SECRET=test 16 - TELEGRAM_BOT_TOKEN=test 16 + TELEGRAM_BOT_TOKEN=test 17 + AXIOM_TOKEN="" 18 + AXIOM_DATASET="" 19 + GCP_PROJECT_ID="yolo" 20 + GCP_LOCATION=us-east-1
+11 -7
apps/workflows/Dockerfile
··· 3 3 # See https://github.com/lenra-io/dofigen 4 4 5 5 # ca-certs 6 - FROM debian@sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b AS ca-certs 6 + FROM debian@sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 AS ca-certs 7 7 LABEL \ 8 - org.opencontainers.image.base.digest="sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b" \ 8 + org.opencontainers.image.base.digest="sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734" \ 9 9 org.opencontainers.image.base.name="docker.io/debian:bullseye-slim" 10 - RUN apt update && apt install -y ca-certificates && update-ca-certificates 10 + RUN apt update && apt install -y ca-certificates curl && update-ca-certificates 11 11 12 12 # docker 13 13 FROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS docker ··· 69 69 --from=install \ 70 70 --link \ 71 71 "/app/node_modules" "/app/node_modules" 72 - RUN bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app 72 + RUN bun build --compile --target bun --sourcemap src/index.ts --outfile=app 73 73 74 74 # libsql 75 75 FROM oven/bun@sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a AS libsql ··· 84 84 RUN bun install 85 85 86 86 # runtime 87 - FROM debian@sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b AS runtime 87 + FROM debian@sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 AS runtime 88 88 LABEL \ 89 89 io.dofigen.version="2.6.0" \ 90 90 org.opencontainers.image.authors="OpenStatus Team" \ 91 - org.opencontainers.image.base.digest="sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b" \ 91 + org.opencontainers.image.base.digest="sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734" \ 92 92 org.opencontainers.image.base.name="docker.io/debian:bullseye-slim" \ 93 93 org.opencontainers.image.description="Background job processing and probe scheduling for OpenStatus" \ 94 94 org.opencontainers.image.source="https://github.com/openstatusHQ/openstatus" \ ··· 116 116 --chown=1000:1000 \ 117 117 --link \ 118 118 "/etc/ssl/certs/ca-certificates.crt" "/etc/ssl/certs/" 119 - RUN apt update && apt install -y curl && rm -rf /var/lib/apt/lists/* 119 + COPY \ 120 + --from=ca-certs \ 121 + --chown=1000:1000 \ 122 + --link \ 123 + "/usr/bin/curl" "/usr/bin/curl" 120 124 USER 1000:1000 121 125 EXPOSE 3000 122 126 ENTRYPOINT ["/app/apps/workflows/app"]
+42 -38
apps/workflows/dofigen.lock
··· 12 12 - /packages/error 13 13 - /packages/tracker 14 14 builders: 15 - ca-certs: 16 - fromImage: 17 - path: debian 18 - digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 19 - label: 20 - org.opencontainers.image.base.name: docker.io/debian:bullseye-slim 21 - org.opencontainers.image.base.digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 22 - run: 23 - - apt update && apt install -y ca-certificates && update-ca-certificates 24 15 install: 25 16 fromImage: 26 17 path: oven/bun ··· 81 72 source: packages/upstash/package.json 82 73 - target: packages/theme-store/package.json 83 74 source: packages/theme-store/package.json 84 - build: 75 + ca-certs: 76 + fromImage: 77 + path: debian 78 + digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 79 + label: 80 + org.opencontainers.image.base.digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 81 + org.opencontainers.image.base.name: docker.io/debian:bullseye-slim 82 + run: 83 + - apt update && apt install -y ca-certificates curl && update-ca-certificates 84 + libsql: 85 85 fromImage: 86 86 path: oven/bun 87 87 digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a 88 88 label: 89 89 org.opencontainers.image.base.digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a 90 - org.opencontainers.image.stage: build 91 90 org.opencontainers.image.base.name: docker.io/oven/bun:1.3.6 92 - workdir: /app/apps/workflows 93 - env: 94 - NODE_ENV: production 91 + workdir: /app/ 95 92 copy: 96 - - paths: 97 - - . 98 - target: /app/ 99 - - fromBuilder: install 93 + - fromBuilder: docker 100 94 paths: 101 - - /app/node_modules 102 - target: /app/node_modules 95 + - /app/apps/build-docker/package.json 96 + target: /app/package.json 103 97 run: 104 - - bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app 98 + - bun install 105 99 docker: 106 100 fromImage: 107 101 path: oven/bun ··· 116 110 target: /app/ 117 111 run: 118 112 - bun run src/build-docker.ts 119 - libsql: 113 + build: 120 114 fromImage: 121 115 path: oven/bun 122 116 digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a 123 117 label: 124 118 org.opencontainers.image.base.digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a 119 + org.opencontainers.image.stage: build 125 120 org.opencontainers.image.base.name: docker.io/oven/bun:1.3.6 126 - workdir: /app/ 121 + workdir: /app/apps/workflows 122 + env: 123 + NODE_ENV: production 127 124 copy: 128 - - fromBuilder: docker 125 + - paths: 126 + - . 127 + target: /app/ 128 + - fromBuilder: install 129 129 paths: 130 - - /app/apps/build-docker/package.json 131 - target: /app/package.json 130 + - /app/node_modules 131 + target: /app/node_modules 132 132 run: 133 - - bun install 133 + - bun build --compile --target bun --sourcemap src/index.ts --outfile=app 134 134 fromImage: 135 135 path: debian 136 136 digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 137 137 label: 138 + org.opencontainers.image.authors: OpenStatus Team 139 + org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus 140 + org.opencontainers.image.title: OpenStatus Workflows 141 + org.opencontainers.image.base.digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 142 + org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus 138 143 io.dofigen.version: 2.6.0 139 144 org.opencontainers.image.vendor: OpenStatus 140 - org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus 141 145 org.opencontainers.image.base.name: docker.io/debian:bullseye-slim 142 - org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus 143 - org.opencontainers.image.base.digest: sha256:b32674fb57780ad57d7b0749242d3f585f462f4ec4a60ae0adacd945f9cb9734 144 - org.opencontainers.image.authors: OpenStatus Team 145 - org.opencontainers.image.title: OpenStatus Workflows 146 146 workdir: /app/ 147 147 copy: 148 148 - fromBuilder: build ··· 162 162 paths: 163 163 - /etc/ssl/certs/ca-certificates.crt 164 164 target: /etc/ssl/certs/ 165 - run: 166 - - apt update && apt install -y curl && rm -rf /var/lib/apt/lists/* 165 + - fromBuilder: ca-certs 166 + paths: 167 + - /usr/bin/curl 168 + target: /usr/bin/curl 167 169 entrypoint: 168 170 - /app/apps/workflows/app 169 171 expose: ··· 180 182 digest: sha256:f20d9cf365ab35529384f1717687c739c92e6f39157a35a95ef06f4049a10e4a 181 183 resources: 182 184 dofigen.yml: 183 - hash: d4c321cbb7593d300b7c6a74e58cb7ef2b3a4f37253386bd348d2d0f844b7248 185 + hash: 070adced2c20f3f63d9a803b4494401f9df769d071fca19c29755f75a0127a06 184 186 content: | 185 187 ignore: 186 188 - node_modules ··· 244 246 # Compile the TypeScript application 245 247 env: 246 248 NODE_ENV: production 247 - run: bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app 249 + run: bun build --compile --target bun --sourcemap src/index.ts --outfile=app 248 250 249 251 docker: 250 252 fromImage: oven/bun:1.3.6 ··· 264 266 265 267 ca-certs: 266 268 fromImage: debian:bullseye-slim 267 - run: apt update && apt install -y ca-certificates && update-ca-certificates 269 + run: apt update && apt install -y ca-certificates curl && update-ca-certificates 268 270 269 271 fromImage: debian:bullseye-slim 270 272 workdir: /app/ ··· 292 294 - fromBuilder: ca-certs 293 295 source: /etc/ssl/certs/ca-certificates.crt 294 296 target: /etc/ssl/certs/ 297 + - fromBuilder: ca-certs 298 + source: /usr/bin/curl 299 + target: /usr/bin/curl 295 300 expose: "3000" 296 - run: apt update && apt install -y curl && rm -rf /var/lib/apt/lists/* 297 301 298 302 entrypoint: /app/apps/workflows/app
+5 -3
apps/workflows/dofigen.yml
··· 60 60 # Compile the TypeScript application 61 61 env: 62 62 NODE_ENV: production 63 - run: bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app 63 + run: bun build --compile --target bun --sourcemap src/index.ts --outfile=app 64 64 65 65 docker: 66 66 fromImage: oven/bun:1.3.6 ··· 80 80 81 81 ca-certs: 82 82 fromImage: debian:bullseye-slim 83 - run: apt update && apt install -y ca-certificates && update-ca-certificates 83 + run: apt update && apt install -y ca-certificates curl && update-ca-certificates 84 84 85 85 fromImage: debian:bullseye-slim 86 86 workdir: /app/ ··· 108 108 - fromBuilder: ca-certs 109 109 source: /etc/ssl/certs/ca-certificates.crt 110 110 target: /etc/ssl/certs/ 111 + - fromBuilder: ca-certs 112 + source: /usr/bin/curl 113 + target: /usr/bin/curl 111 114 expose: "3000" 112 - run: apt update && apt install -y curl && rm -rf /var/lib/apt/lists/* 113 115 114 116 entrypoint: /app/apps/workflows/app
+3 -2
apps/workflows/package.json
··· 9 9 "@google-cloud/tasks": "4.0.1", 10 10 "@hono/sentry": "1.2.2", 11 11 "@libsql/client": "0.15.15", 12 - "@logtape/logtape": "1.1.2", 13 - "@logtape/sentry": "1.1.2", 12 + "@logtape/logtape": "2.0.0", 13 + "@logtape/otel": "2.0.0", 14 + "@logtape/sentry": "2.0.0", 14 15 "@openstatus/db": "workspace:*", 15 16 "@openstatus/emails": "workspace:*", 16 17 "@openstatus/notification-discord": "workspace:*",
+37 -9
apps/workflows/src/checker/alerting.ts
··· 33 33 region?: Region; 34 34 latency?: number; 35 35 }) => { 36 - console.log(`💌 triggerAlerting for ${monitorId}`); 36 + logger.info("Triggering alerting", { 37 + monitor_id: monitorId, 38 + notification_type: notifType, 39 + }); 37 40 const notifications = await db 38 41 .select() 39 42 .from(schema.notificationsToMonitors) ··· 100 103 continue; 101 104 } 102 105 } 103 - logger.info( 104 - `💌 sending notification for ${monitorId} and chanel ${notif.notification.provider} for ${notifType}`, 105 - ); 106 + logger.info("Sending notification", { 107 + monitor_id: monitorId, 108 + provider: notif.notification.provider, 109 + notification_type: notifType, 110 + notification_id: notif.notification.id, 111 + }); 106 112 const monitor = selectMonitorSchema.parse(notif.monitor); 107 113 try { 108 114 await insertNotificationTrigger({ ··· 139 145 schedule: Schedule.exponential("1000 millis"), 140 146 }), 141 147 ); 142 - await Effect.runPromise(alertResult).catch(console.error); 148 + await Effect.runPromise(alertResult).catch((err) => 149 + logger.error("Failed to send alert notification", { 150 + monitor_id: monitorId, 151 + provider: notif.notification.provider, 152 + error_message: err instanceof Error ? err.message : String(err), 153 + }), 154 + ); 143 155 break; 144 156 case "recovery": 145 157 const recoveryResult = Effect.tryPromise({ ··· 164 176 schedule: Schedule.exponential("1000 millis"), 165 177 }), 166 178 ); 167 - await Effect.runPromise(recoveryResult).catch(console.error); 179 + await Effect.runPromise(recoveryResult).catch((err) => 180 + logger.error("Failed to send recovery notification", { 181 + monitor_id: monitorId, 182 + provider: notif.notification.provider, 183 + error_message: err instanceof Error ? err.message : String(err), 184 + }), 185 + ); 168 186 break; 169 187 case "degraded": 170 188 const degradedResult = Effect.tryPromise({ ··· 189 207 schedule: Schedule.exponential("1000 millis"), 190 208 }), 191 209 ); 192 - await Effect.runPromise(degradedResult).catch(console.error); 210 + await Effect.runPromise(degradedResult).catch((err) => 211 + logger.error("Failed to send degraded notification", { 212 + monitor_id: monitorId, 213 + provider: notif.notification.provider, 214 + error_message: err instanceof Error ? err.message : String(err), 215 + }), 216 + ); 193 217 break; 194 218 } 195 219 // ALPHA ··· 246 270 set: { status, updatedAt: new Date() }, 247 271 }) 248 272 .returning(); 249 - logger.info(`📈 upsertMonitorStatus for ${monitorId} in region ${region}`); 250 - logger.info("🤔 upsert monitor {*}", { ...newData }); 273 + logger.debug("Upserted monitor status", { 274 + monitor_id: monitorId, 275 + region, 276 + status, 277 + updated_at: newData[0]?.updatedAt, 278 + }); 251 279 };
+36 -6
apps/workflows/src/checker/index.ts
··· 11 11 import { getLogger } from "@logtape/logtape"; 12 12 import { monitorRegions } from "@openstatus/db/src/schema/constants"; 13 13 import { env } from "../env"; 14 + import type { Env } from "../index"; 14 15 import { checkerAudit } from "../utils/audit-log"; 15 16 import { triggerNotifications, upsertMonitorStatus } from "./alerting"; 16 17 17 - export const checkerRoute = new Hono(); 18 + export const checkerRoute = new Hono<Env>(); 18 19 19 20 const payloadSchema = z.object({ 20 21 monitorId: z.string(), ··· 35 36 return c.text("Unauthorized", 401); 36 37 } 37 38 39 + const event = c.get("event"); 38 40 const json = await c.req.json(); 39 41 40 42 const result = payloadSchema.safeParse(json); ··· 42 44 if (!result.success) { 43 45 return c.text("Unprocessable Entity", 422); 44 46 } 47 + event.status_update = { 48 + status: result.data.status, 49 + message: result.data.message, 50 + region: result.data.region, 51 + status_code: result.data.statusCode, 52 + cron_timestamp: result.data.cronTimestamp, 53 + latency_ms: result.data.latency, 54 + }; 55 + 45 56 const { 46 57 monitorId, 47 58 message, ··· 52 63 latency, 53 64 } = result.data; 54 65 55 - logger.info("📝 update monitor status {*}", { ...result.data }); 66 + logger.info("Updating monitor status", { 67 + monitor_id: monitorId, 68 + region, 69 + status, 70 + status_code: statusCode, 71 + cron_timestamp: cronTimestamp, 72 + latency_ms: latency, 73 + }); 56 74 57 75 // First we upsert the monitor status 58 76 await upsertMonitorStatus({ ··· 139 157 break; 140 158 } 141 159 142 - logger.info(`🔄 update monitorStatus ${monitor.id} status: ACTIVE`); 160 + logger.info("Monitor status changed to active", { 161 + monitor_id: monitor.id, 162 + workspace_id: monitor.workspaceId, 163 + }); 143 164 await db 144 165 .update(schema.monitor) 145 166 .set({ status: "active" }) ··· 167 188 // incident is already resolved 168 189 break; 169 190 } 170 - logger.info(`🤓 recovering incident ${incident.id}`); 191 + logger.info("Recovering incident", { 192 + incident_id: incident.id, 193 + monitor_id: monitorId, 194 + }); 171 195 172 196 await db 173 197 .update(incidentTable) ··· 204 228 // already degraded let's return early 205 229 break; 206 230 } 207 - logger.info(`🔄 update monitorStatus ${monitor.id} status: DEGRADED`); 231 + logger.info("Monitor status changed to degraded", { 232 + monitor_id: monitor.id, 233 + workspace_id: monitor.workspaceId, 234 + }); 208 235 209 236 await db 210 237 .update(schema.monitor) ··· 229 256 break; 230 257 } 231 258 232 - logger.info(`🔄 update monitorStatus ${monitor.id} status: ERROR`); 259 + logger.info("Monitor status changed to error", { 260 + monitor_id: monitor.id, 261 + workspace_id: monitor.workspaceId, 262 + }); 233 263 234 264 await db 235 265 .update(schema.monitor)
+19 -8
apps/workflows/src/cron/checker.ts
··· 93 93 ) 94 94 .all(); 95 95 96 - logger.info(`Start cron for ${periodicity}`); 96 + logger.info("Starting cron job", { 97 + periodicity, 98 + monitor_count: result.length, 99 + }); 97 100 98 101 const monitors = z.array(selectMonitorSchema).safeParse(result); 99 102 const allResult = []; ··· 112 115 .all(); 113 116 const monitorStatus = z.array(selectMonitorStatusSchema).safeParse(result); 114 117 if (!monitorStatus.success) { 115 - console.error( 116 - `Error while fetching the monitor status ${monitorStatus.error}`, 117 - ); 118 + logger.error("Failed to parse monitor status", { 119 + monitor_id: row.id, 120 + error_message: monitorStatus.error.message, 121 + }); 118 122 continue; 119 123 } 120 124 ··· 164 168 const success = allRequests.filter((r) => r.status === "fulfilled").length; 165 169 const failed = allRequests.filter((r) => r.status === "rejected").length; 166 170 167 - logger.info( 168 - `End cron for ${periodicity} with ${allResult.length} jobs with ${success} success and ${failed} failed`, 169 - ); 171 + logger.info("Completed cron job", { 172 + periodicity, 173 + total_tasks: allResult.length, 174 + success_count: success, 175 + failed_count: failed, 176 + }); 170 177 if (failed > 0) { 171 - logger.error("error with cron jobs"); 178 + logger.error("Cron job had failures", { 179 + periodicity, 180 + failed_count: failed, 181 + success_count: success, 182 + }); 172 183 getSentry(c).captureMessage( 173 184 `sendCheckerTasks for ${periodicity} ended with ${failed} failed tasks`, 174 185 "error",
+2
apps/workflows/src/env.ts
··· 20 20 TWILLIO_AUTH_TOKEN: z.string().prefault(""), 21 21 TWILLIO_ACCOUNT_ID: z.string().prefault(""), 22 22 SENTRY_DSN: z.string().prefault(""), 23 + AXIOM_TOKEN: z.string().prefault(""), 24 + AXIOM_DATASET: z.string().prefault(""), 23 25 }) 24 26 .parse(process.env);
+108 -19
apps/workflows/src/index.ts
··· 2 2 // import * as Sentry from "@sentry/node"; 3 3 import { sentry } from "@hono/sentry"; 4 4 import { 5 - configureSync, 5 + configure, 6 6 getConsoleSink, 7 7 getLogger, 8 8 jsonLinesFormatter, 9 9 withContext, 10 10 } from "@logtape/logtape"; 11 + import { getOpenTelemetrySink } from "@logtape/otel"; 12 + 11 13 // import { getSentrySink } from "@logtape/sentry"; 12 14 import { Hono } from "hono"; 13 15 import { showRoutes } from "hono/dev"; ··· 17 19 import { cronRouter } from "./cron"; 18 20 import { env } from "./env"; 19 21 22 + import { resourceFromAttributes } from "@opentelemetry/resources"; 23 + import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from "@opentelemetry/semantic-conventions/incubating"; 24 + 20 25 const { NODE_ENV, PORT } = env(); 21 26 22 - configureSync({ 27 + export type Env = { 28 + Variables: { 29 + event: Record<string, unknown>; 30 + }; 31 + }; 32 + 33 + /** 34 + * Tail sampling strategy based on loggingsucks.com best practices 35 + * Makes sampling decisions post-request completion to capture: 36 + * - All errors (5xx status codes, explicit errors) 37 + * - Slow requests (above p99 threshold) 38 + * - Client errors (4xx) at higher rate than successful requests 39 + * - Random sample of remaining successful, fast requests 40 + */ 41 + function shouldSample(event: Record<string, unknown>): boolean { 42 + const statusCode = event.status_code as number | undefined; 43 + const durationMs = event.duration_ms as number | undefined; 44 + 45 + // Always capture: server errors 46 + if (statusCode && statusCode >= 500) return true; 47 + 48 + // Always capture: explicit errors 49 + if (event.error) return true; 50 + 51 + // Always capture: slow requests (above p99 - 2s threshold) 52 + if (durationMs && durationMs > 2000) return true; 53 + 54 + // Higher sampling for client errors (4xx) - 50% 55 + if (statusCode && statusCode >= 400 && statusCode < 500) { 56 + return true; 57 + } 58 + 59 + // Random sample successful, fast requests at 5% 60 + return Math.random() < 0.05; 61 + } 62 + 63 + const defaultLogger = getOpenTelemetrySink({ 64 + serviceName: "openstatus-workflows", 65 + otlpExporterConfig: { 66 + url: "https://eu-central-1.aws.edge.axiom.co/v1/logs", 67 + headers: { 68 + Authorization: `Bearer ${env().AXIOM_TOKEN}`, 69 + "X-Axiom-Dataset": env().AXIOM_DATASET, 70 + }, 71 + }, 72 + additionalResource: resourceFromAttributes({ 73 + [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: env().NODE_ENV, 74 + }), 75 + }); 76 + 77 + await configure({ 23 78 sinks: { 24 79 console: getConsoleSink({ formatter: jsonLinesFormatter }), 25 80 // sentry: getSentrySink(), 81 + otel: defaultLogger, 26 82 }, 27 83 loggers: [ 28 84 { ··· 30 86 lowestLevel: "debug", 31 87 sinks: ["console"], 32 88 }, 89 + { 90 + category: "workflow-otel", 91 + lowestLevel: "info", 92 + sinks: ["otel"], 93 + }, 33 94 ], 34 95 contextLocalStorage: new AsyncLocalStorage(), 35 96 }); 36 97 37 98 const logger = getLogger(["workflow"]); 38 - const app = new Hono({ strict: false }); 99 + const otelLogger = getLogger(["workflow-otel"]); 100 + 101 + const app = new Hono<Env>({ strict: false }); 39 102 40 103 app.use("*", requestId()); 41 104 ··· 45 108 const requestId = c.get("requestId"); 46 109 const startTime = Date.now(); 47 110 111 + const event: Record<string, unknown> = { 112 + timestamp: new Date().toISOString(), 113 + }; 114 + c.set("event", event); 115 + 48 116 await withContext( 49 117 { 50 118 requestId, ··· 54 122 // ipAddress: c.req.header("CF-Connecting-IP") || c.req.header("X-Forwarded-For") 55 123 }, 56 124 async () => { 57 - logger.info("Request started", { 58 - method: c.req.method, 59 - url: c.req.url, 60 - requestId, 61 - }); 125 + // Build wide event context at request start 126 + event.request_id = requestId; 127 + event.method = c.req.method; 128 + event.path = c.req.path; 129 + event.url = c.req.url; 130 + event.user_agent = c.req.header("User-Agent"); 131 + event.content_type = c.req.header("Content-Type"); 132 + event.cf_ray = c.req.header("CF-Ray"); 133 + event.cf_connecting_ip = c.req.header("CF-Connecting-IP"); 62 134 63 135 await next(); 64 136 65 137 const duration = Date.now() - startTime; 66 - logger.info("Request completed", { 67 - status: c.res.status, 68 - duration, 69 - requestId, 138 + 139 + event.status_code = c.res.status; 140 + if (c.error) { 141 + event.outcome = "error"; 142 + event.error = { 143 + type: c.error.name, 144 + message: c.error.message, 145 + stack: c.error.stack, 146 + }; 147 + } else { 148 + event.outcome = "success"; 149 + } 150 + event.duration_ms = duration; 151 + // Emit canonical log line with all context (wide event pattern) 152 + if (shouldSample(event)) { 153 + otelLogger.info("request", event); 154 + } 155 + logger.debug("Request completed", { 156 + status_code: c.res.status, 157 + duration_ms: duration, 158 + request_id: requestId, 70 159 }); 71 160 }, 72 161 ); 73 162 }); 74 163 75 164 app.onError((err, c) => { 76 - logger.error("Request error", { 77 - error: { 78 - name: err.name, 79 - message: err.message, 80 - stack: err.stack, 81 - }, 165 + logger.error("Unhandled request error", { 166 + error_name: err.name, 167 + error_message: err.message, 168 + error_stack: err.stack, 82 169 method: c.req.method, 170 + path: c.req.path, 83 171 url: c.req.url, 172 + request_id: c.get("requestId"), 84 173 }); 85 174 c.get("sentry").captureException(err); 86 175 ··· 105 194 showRoutes(app, { verbose: true, colorize: true }); 106 195 } 107 196 108 - console.log(`Starting server on port ${PORT}`); 197 + logger.info("Starting server", { port: PORT, environment: NODE_ENV }); 109 198 110 199 const server = { port: PORT, fetch: app.fetch }; 111 200
+2 -2
apps/workflows/src/lib/db.ts
··· 5 5 import { env } from "../env"; 6 6 7 7 const file = 8 - env().NODE_ENV === "development" ? "./dev.db" : "/app/data/replica.db"; 8 + env().NODE_ENV === "development" ? "./dev.db" : "///app/data/replica.db"; 9 9 const client = createClient({ 10 - url: `file://${file}`, 10 + url: `file:${file}`, 11 11 syncUrl: env().DATABASE_URL, 12 12 authToken: env().DATABASE_AUTH_TOKEN, 13 13 syncInterval: 60,
+4 -1
apps/workflows/tsconfig.json
··· 3 3 "compilerOptions": { 4 4 "jsx": "react-jsx", 5 5 "jsxImportSource": "hono/jsx", 6 - "lib": ["ES2022"] 6 + "lib": ["ES2022"], 7 + "module": "ESNext", 8 + "target": "ESNext", 9 + "moduleResolution": "bundler" 7 10 } 8 11 }
+169 -12
pnpm-lock.yaml
··· 453 453 specifier: 0.7.6 454 454 version: 0.7.6(hono@4.11.3)(zod@4.1.13) 455 455 '@logtape/logtape': 456 - specifier: 1.1.2 457 - version: 1.1.2 456 + specifier: 2.0.0 457 + version: 2.0.0 458 + '@logtape/otel': 459 + specifier: 2.0.0 460 + version: 2.0.0(@logtape/logtape@2.0.0) 458 461 '@logtape/sentry': 459 462 specifier: 1.1.2 460 - version: 1.1.2(@logtape/logtape@1.1.2) 463 + version: 1.1.2(@logtape/logtape@2.0.0) 461 464 '@openstatus/analytics': 462 465 specifier: workspace:* 463 466 version: link:../../packages/analytics ··· 488 491 '@openstatus/utils': 489 492 specifier: workspace:* 490 493 version: link:../../packages/utils 494 + '@opentelemetry/resources': 495 + specifier: 2.2.0 496 + version: 2.2.0(@opentelemetry/api@1.9.0) 497 + '@opentelemetry/semantic-conventions': 498 + specifier: 1.38.0 499 + version: 1.38.0 491 500 '@scalar/hono-api-reference': 492 501 specifier: 0.8.5 493 502 version: 0.8.5(hono@4.11.3) ··· 1086 1095 specifier: 0.15.15 1087 1096 version: 0.15.15 1088 1097 '@logtape/logtape': 1089 - specifier: 1.1.2 1090 - version: 1.1.2 1098 + specifier: 2.0.0 1099 + version: 2.0.0 1100 + '@logtape/otel': 1101 + specifier: 2.0.0 1102 + version: 2.0.0(@logtape/logtape@2.0.0) 1091 1103 '@logtape/sentry': 1092 - specifier: 1.1.2 1093 - version: 1.1.2(@logtape/logtape@1.1.2) 1104 + specifier: 2.0.0 1105 + version: 2.0.0(@logtape/logtape@2.0.0)(@sentry/core@10.31.0) 1094 1106 '@openstatus/db': 1095 1107 specifier: workspace:* 1096 1108 version: link:../../packages/db ··· 3586 3598 cpu: [x64] 3587 3599 os: [win32] 3588 3600 3589 - '@logtape/logtape@1.1.2': 3590 - resolution: {integrity: sha512-4QXa/5cwt3+cx01C1xm8iqvEXxNZMDTTbT5AFGZnwPkn2WDAyA6UP2khWBZ7FctMnvQqW3fnJ96kgBJ4ahorHw==} 3601 + '@logtape/logtape@2.0.0': 3602 + resolution: {integrity: sha512-z9Hp44mIRXAzgxSyQfFQiRuJ78EMnZa6g43UCxyGOO3RgHjn/7q+5OhdbhypkeHjiJRPxv6RmRsyF0S+OOYWnA==} 3603 + 3604 + '@logtape/otel@2.0.0': 3605 + resolution: {integrity: sha512-2lsip757ZwjMYkINJYcljYf7fd+Hvv3cstF4A3oI3Kbs5yw2f2h7eBhJbHCxKuTj5mJp1vWzuEFrxI94F9jdCA==} 3606 + peerDependencies: 3607 + '@logtape/logtape': ^2.0.0 3591 3608 3592 3609 '@logtape/sentry@1.1.2': 3593 3610 resolution: {integrity: sha512-Hmt1traui55R6BvDh8Ug4yXUJVJst9guW3TyA5CyFjGUIvgxmV9uukN6q0rMVfjB1YGxMUPrRYI8ddtYu5RJDg==} 3594 3611 peerDependencies: 3595 3612 '@logtape/logtape': ^1.1.2 3613 + 3614 + '@logtape/sentry@2.0.0': 3615 + resolution: {integrity: sha512-Z3Nlfd6KLGd9a8qVUSThsXR7H2Pxm3Wu2e3nPC2QIE0Q8eL5xQ/8+JN/wsqO99EiX88dJibvgkZOKeIuqFEtVQ==} 3616 + peerDependencies: 3617 + '@logtape/logtape': ^2.0.0 3618 + '@sentry/core': '>=8.0.0' 3596 3619 3597 3620 '@mdx-js/mdx@3.1.1': 3598 3621 resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} ··· 3736 3759 peerDependencies: 3737 3760 '@opentelemetry/api': '>=1.0.0 <1.10.0' 3738 3761 3762 + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0': 3763 + resolution: {integrity: sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg==} 3764 + engines: {node: ^18.19.0 || >=20.6.0} 3765 + peerDependencies: 3766 + '@opentelemetry/api': ^1.3.0 3767 + 3768 + '@opentelemetry/exporter-logs-otlp-http@0.208.0': 3769 + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} 3770 + engines: {node: ^18.19.0 || >=20.6.0} 3771 + peerDependencies: 3772 + '@opentelemetry/api': ^1.3.0 3773 + 3774 + '@opentelemetry/exporter-logs-otlp-proto@0.208.0': 3775 + resolution: {integrity: sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ==} 3776 + engines: {node: ^18.19.0 || >=20.6.0} 3777 + peerDependencies: 3778 + '@opentelemetry/api': ^1.3.0 3779 + 3739 3780 '@opentelemetry/instrumentation-amqplib@0.55.0': 3740 3781 resolution: {integrity: sha512-5ULoU8p+tWcQw5PDYZn8rySptGSLZHNX/7srqo2TioPnAAcvTy6sQFQXsNPrAnyRRtYGMetXVyZUy5OaX1+IfA==} 3741 3782 engines: {node: ^18.19.0 || >=20.6.0} ··· 3874 3915 peerDependencies: 3875 3916 '@opentelemetry/api': ^1.3.0 3876 3917 3918 + '@opentelemetry/otlp-exporter-base@0.208.0': 3919 + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} 3920 + engines: {node: ^18.19.0 || >=20.6.0} 3921 + peerDependencies: 3922 + '@opentelemetry/api': ^1.3.0 3923 + 3924 + '@opentelemetry/otlp-grpc-exporter-base@0.208.0': 3925 + resolution: {integrity: sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ==} 3926 + engines: {node: ^18.19.0 || >=20.6.0} 3927 + peerDependencies: 3928 + '@opentelemetry/api': ^1.3.0 3929 + 3930 + '@opentelemetry/otlp-transformer@0.208.0': 3931 + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} 3932 + engines: {node: ^18.19.0 || >=20.6.0} 3933 + peerDependencies: 3934 + '@opentelemetry/api': ^1.3.0 3935 + 3877 3936 '@opentelemetry/redis-common@0.38.2': 3878 3937 resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} 3879 3938 engines: {node: ^18.19.0 || >=20.6.0} ··· 3883 3942 engines: {node: ^18.19.0 || >=20.6.0} 3884 3943 peerDependencies: 3885 3944 '@opentelemetry/api': '>=1.3.0 <1.10.0' 3945 + 3946 + '@opentelemetry/sdk-logs@0.208.0': 3947 + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} 3948 + engines: {node: ^18.19.0 || >=20.6.0} 3949 + peerDependencies: 3950 + '@opentelemetry/api': '>=1.4.0 <1.10.0' 3951 + 3952 + '@opentelemetry/sdk-metrics@2.2.0': 3953 + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} 3954 + engines: {node: ^18.19.0 || >=20.6.0} 3955 + peerDependencies: 3956 + '@opentelemetry/api': '>=1.9.0 <1.10.0' 3886 3957 3887 3958 '@opentelemetry/sdk-trace-base@2.2.0': 3888 3959 resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} ··· 13296 13367 '@libsql/win32-x64-msvc@0.5.22': 13297 13368 optional: true 13298 13369 13299 - '@logtape/logtape@1.1.2': {} 13370 + '@logtape/logtape@2.0.0': {} 13300 13371 13301 - '@logtape/sentry@1.1.2(@logtape/logtape@1.1.2)': 13372 + '@logtape/otel@2.0.0(@logtape/logtape@2.0.0)': 13302 13373 dependencies: 13303 - '@logtape/logtape': 1.1.2 13374 + '@logtape/logtape': 2.0.0 13375 + '@opentelemetry/api': 1.9.0 13376 + '@opentelemetry/api-logs': 0.208.0 13377 + '@opentelemetry/exporter-logs-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) 13378 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) 13379 + '@opentelemetry/exporter-logs-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) 13380 + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 13381 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 13382 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 13383 + '@opentelemetry/semantic-conventions': 1.38.0 13384 + 13385 + '@logtape/sentry@1.1.2(@logtape/logtape@2.0.0)': 13386 + dependencies: 13387 + '@logtape/logtape': 2.0.0 13304 13388 '@sentry/core': 9.47.1 13389 + 13390 + '@logtape/sentry@2.0.0(@logtape/logtape@2.0.0)(@sentry/core@10.31.0)': 13391 + dependencies: 13392 + '@logtape/logtape': 2.0.0 13393 + '@sentry/core': 10.31.0 13305 13394 13306 13395 '@mdx-js/mdx@3.1.1': 13307 13396 dependencies: ··· 13457 13546 '@opentelemetry/api': 1.9.0 13458 13547 '@opentelemetry/semantic-conventions': 1.38.0 13459 13548 13549 + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': 13550 + dependencies: 13551 + '@grpc/grpc-js': 1.14.1 13552 + '@opentelemetry/api': 1.9.0 13553 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13554 + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 13555 + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 13556 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 13557 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 13558 + 13559 + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': 13560 + dependencies: 13561 + '@opentelemetry/api': 1.9.0 13562 + '@opentelemetry/api-logs': 0.208.0 13563 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13564 + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 13565 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 13566 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 13567 + 13568 + '@opentelemetry/exporter-logs-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': 13569 + dependencies: 13570 + '@opentelemetry/api': 1.9.0 13571 + '@opentelemetry/api-logs': 0.208.0 13572 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13573 + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 13574 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 13575 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 13576 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 13577 + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) 13578 + 13460 13579 '@opentelemetry/instrumentation-amqplib@0.55.0(@opentelemetry/api@1.9.0)': 13461 13580 dependencies: 13462 13581 '@opentelemetry/api': 1.9.0 ··· 13651 13770 transitivePeerDependencies: 13652 13771 - supports-color 13653 13772 13773 + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': 13774 + dependencies: 13775 + '@opentelemetry/api': 1.9.0 13776 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13777 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 13778 + 13779 + '@opentelemetry/otlp-grpc-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': 13780 + dependencies: 13781 + '@grpc/grpc-js': 1.14.1 13782 + '@opentelemetry/api': 1.9.0 13783 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13784 + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 13785 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 13786 + 13787 + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': 13788 + dependencies: 13789 + '@opentelemetry/api': 1.9.0 13790 + '@opentelemetry/api-logs': 0.208.0 13791 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13792 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 13793 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 13794 + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) 13795 + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) 13796 + protobufjs: 7.5.4 13797 + 13654 13798 '@opentelemetry/redis-common@0.38.2': {} 13655 13799 13656 13800 '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': ··· 13658 13802 '@opentelemetry/api': 1.9.0 13659 13803 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13660 13804 '@opentelemetry/semantic-conventions': 1.38.0 13805 + 13806 + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': 13807 + dependencies: 13808 + '@opentelemetry/api': 1.9.0 13809 + '@opentelemetry/api-logs': 0.208.0 13810 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13811 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 13812 + 13813 + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': 13814 + dependencies: 13815 + '@opentelemetry/api': 1.9.0 13816 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 13817 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 13661 13818 13662 13819 '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': 13663 13820 dependencies: