diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13ce873..6928db9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,13 +18,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: '.nvmrc' - name: Run pnpm install @@ -34,7 +34,7 @@ jobs: run: pnpm build - name: Upload build output - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: build-output path: 'dist' @@ -52,18 +52,18 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_TF_DEV_ENV }} steps: - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Get build artifact - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: 'build-output' path: '${{ github.workspace }}/dist' - name: Install 1Password CLI - uses: 1password/install-cli-action@9a0c9dd934086b7ab1d90115d455bda1c53c2bdb # v2.0.2 + uses: 1password/install-cli-action@8d006a0d0a4fd505af7f7ce589e7f768385ff5e4 # v3.0.0 - name: Setup Mise uses: immich-app/devtools/actions/use-mise@697a75e2c3186d3c037c2c159855cf2d566542ba # use-mise-action-0.0.1 @@ -100,7 +100,7 @@ jobs: - name: Comment preview URLs if: ${{ github.event_name == 'pull_request' }} - uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 + uses: actions-cool/maintain-one-comment@772214ac4bff3c578c76eed4ac19d08dc787c050 # v3.2.1 with: number: ${{ github.event.number }} body: ${{ steps.preview-urls.outputs.body }} @@ -120,18 +120,18 @@ jobs: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_TF_PROD_ENV }} steps: - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Get build artifact - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: 'build-output' path: '${{ github.workspace }}/dist' - name: Install 1Password CLI - uses: 1password/install-cli-action@9a0c9dd934086b7ab1d90115d455bda1c53c2bdb # v2.0.2 + uses: 1password/install-cli-action@8d006a0d0a4fd505af7f7ce589e7f768385ff5e4 # v3.0.0 - name: Setup Mise uses: immich-app/devtools/actions/use-mise@697a75e2c3186d3c037c2c159855cf2d566542ba # use-mise-action-0.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50224ee..d2cb528 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,11 +15,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: '.nvmrc' @@ -41,3 +43,64 @@ jobs: - name: Run unit tests run: pnpm run test if: ${{ !cancelled() }} + + integration-test-cloudflare-metrics: + name: Integration Test (cloudflare-metrics) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Detect relevant changes + id: changes + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + with: + filters: | + cloudflare-metrics: + - 'apps/cloudflare-metrics/**' + - '.github/workflows/test.yml' + + - name: Setup pnpm + if: steps.changes.outputs.cloudflare-metrics == 'true' || github.event_name != 'pull_request' + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + + - name: Setup Node + if: steps.changes.outputs.cloudflare-metrics == 'true' || github.event_name != 'pull_request' + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + + - name: Run pnpm install + if: steps.changes.outputs.cloudflare-metrics == 'true' || github.event_name != 'pull_request' + run: pnpm install --frozen-lockfile + + - name: Install 1Password CLI + if: steps.changes.outputs.cloudflare-metrics == 'true' || github.event_name != 'pull_request' + uses: 1password/install-cli-action@8d006a0d0a4fd505af7f7ce589e7f768385ff5e4 # v3.0.0 + + - name: Check integration credentials are available + id: creds + if: steps.changes.outputs.cloudflare-metrics == 'true' || github.event_name != 'pull_request' + continue-on-error: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_TF_DEV_ENV }} + run: | + if op run --env-file=apps/cloudflare-metrics/.integration.env -- sh -c 'test -n "$CLOUDFLARE_API_TOKEN" && test -n "$CLOUDFLARE_ACCOUNT_ID"' 2>/dev/null; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "::warning::Skipping integration test — credentials not configured in 1Password" + fi + + - name: Run integration tests + if: >- + (steps.changes.outputs.cloudflare-metrics == 'true' || github.event_name != 'pull_request') + && steps.creds.outputs.available == 'true' + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_TF_DEV_ENV }} + run: | + op run \ + --env-file=apps/cloudflare-metrics/.integration.env \ + -- pnpm --filter @immich-services/cloudflare-metrics run test:integration diff --git a/.gitignore b/.gitignore index a62a9fb..7b3f8b9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ build/ .wrangler/ .dev.vars +# Local-only env files (the tracked one is `deployment/.env`) +deployment/.env.* + # Logs logs/ *.log @@ -42,3 +45,4 @@ npm-debug.log* # Misc .cache/ tmp/ +.claude/ diff --git a/apps/cloudflare-metrics/.integration.env b/apps/cloudflare-metrics/.integration.env new file mode 100644 index 0000000..edfa1ed --- /dev/null +++ b/apps/cloudflare-metrics/.integration.env @@ -0,0 +1,12 @@ +# 1Password-templated env file loaded by `op run` in CI. +# +# To run integration tests locally: +# op run --env-file=apps/cloudflare-metrics/.integration.env -- \ +# pnpm --filter @immich-services/cloudflare-metrics run test:integration +# +# The 1Password item and field names below are placeholders — set them up +# in the service account's vault before the CI job can run. Both need to +# map to a Cloudflare API token that has `Account Analytics:Read` and +# `Zone:Read` for the dev account. +CLOUDFLARE_API_TOKEN="op://services-cf-workers-dev/cloudflare-metrics-integration/api_token" +CLOUDFLARE_ACCOUNT_ID="op://services-cf-workers-dev/cloudflare-metrics-integration/account_id" diff --git a/apps/cloudflare-metrics/README.md b/apps/cloudflare-metrics/README.md new file mode 100644 index 0000000..bf74c3f --- /dev/null +++ b/apps/cloudflare-metrics/README.md @@ -0,0 +1,190 @@ +# cloudflare-metrics + +Cloudflare Worker that pulls analytics from the Cloudflare GraphQL API every minute, enriches with resource names from the REST API, and writes to VictoriaMetrics as InfluxDB line protocol. + +## Architecture + +``` + Cloudflare GraphQL Analytics API ──► CloudflareGraphQLClient + Cloudflare REST API (D1/queues/zones) ──► CloudflareRestClient + │ + ▼ + CloudflareMetricsCollector + ├─ ResourceCacheService (id → name lookups) + ├─ emit.ts (row → Metric translation) + └─ graphql-builders.ts (query construction) + │ + ▼ + InfluxMetricsProvider ──► VictoriaMetrics /write +``` + +## Collection window + +| Setting | Value | Why | +| ------- | ----------- | ------------------------------------------------------------- | +| Cron | `* * * * *` | Every minute | +| Lag | 5 min | Cloudflare's analytics pipeline is 2–5 min behind real time | +| Window | 3 min | Overlaps consecutive ticks so a missed cron doesn't drop data | +| Dedup | Free | VictoriaMetrics dedupes on `(series, timestamp)` | + +## Datasets + +78 datasets across account-scope and zone-scope, covering: + +- **Workers**: invocations, subrequests, overview, scheduled (client-side aggregated), analytics engine, builds, VPC, placement, workflows +- **D1**: queries (summary + detail with p50/p95/p99), storage +- **R2**: operations, storage, sippy +- **KV**: operations, storage +- **Durable Objects**: invocations, periodic, storage, SQL storage, subrequests +- **Queues**: operations, backlog, consumer concurrency +- **Hyperdrive**: queries (with count), pool sizes +- **HTTP (zone-scope)**: overview, detail (per-zone batched), cache reserve, logpush health +- **Pages Functions**: invocations with CPU/duration p50/p99 +- **AI**: inference, gateway (requests/cache/errors/size), search, autoRAG +- **Vectorize**: operations, queries, storage, writes +- **Browser**: rendering API/sessions/time/events, isolation sessions/actions +- **Stream/Video/Calls**: minutes viewed, CMCD, buffer/playback/quality events, live input, realtime kit, calls usage/TURN +- **Images/toMarkdown**: request counts, conversion stats +- **RUM**: pageload, performance (with FCP/page-load p50/p95/p99), web vitals (CLS/FCP/FID/INP/LCP/TTFB averages + p75/p95) +- **Pipelines**: ingestion, delivery, operator, sink +- **Containers, Turnstile** +- **DNS, Email Routing/Sending, DMARC** (zone-scope) +- **API Gateway sessions, Workers Zone invocations/subrequests** (zone-scope) + +See `src/datasets.ts` for the full registry. Each dataset declares its GraphQL field, dimensions, aggregation blocks, tag mappings, and field mappings. + +### Skipped datasets + +| Dataset | Reason | +| ------------------------------------- | -------------------------------------------------------------------------- | +| `firewallEventsAdaptiveGroups` | Plan-gated (Business/Enterprise only) | +| `cdnNetworkAnalyticsAdaptiveGroups` | Plan-gated | +| `zarazTrack/TriggersAdaptiveGroups` | Incompatible filter shape (`datetimeMinute_geq` instead of `datetime_geq`) | +| `cloudchamberMetricsAdaptiveGroups` | Duplicate schema of `containersMetrics` | +| `cacheReserveRequestsAdaptiveGroups` | Plan-gated (zone-scope) | +| `healthCheckEventsAdaptiveGroups` | Plan-gated (zone-scope) | +| `loadBalancingRequestsAdaptiveGroups` | Plan-gated (zone-scope) | +| `nelReportsAdaptiveGroups` | Plan-gated (zone-scope) | +| `pageShieldReportsAdaptiveGroups` | Plan-gated (zone-scope) | +| `waitingRoomAnalyticsAdaptiveGroups` | Plan-gated (zone-scope) | + +## Query batching + +Cloudflare Workers caps subrequests at 50 per invocation. Account-scope datasets are batched into chunks of 25 using GraphQL aliases. Zone-scope datasets are batched across all zones in a single request per dataset. + +| Metric | Cold start | Warm (cached) | +| ------------------------------ | ---------- | -------------------- | +| REST lookups (D1/queues/zones) | 3 | 0 (10-min TTL cache) | +| GraphQL account batches | 3 chunks | 3 chunks | +| GraphQL date-granularity batch | 1 | 1 | +| Zone-scope datasets | ~11 | ~11 | +| Metric flush | 1 | 1 | +| **Total subrequests** | **~19** | **~16** | + +## Resource name enrichment + +IDs in the analytics API are enriched with human-readable names via REST lookups: + +- `database_name` on `cf_d1_*` metrics (from `/accounts/{id}/d1/database`) +- `queue_name` on `cf_queue_*` metrics (from `/accounts/{id}/queues`) +- `zone_name` on `cf_http_*` metrics (from `/zones?account.id={id}` + per-zone fallback for Pages projects) + +Module-level caches survive across isolate invocations (typically 10+ minutes on the paid plan). A 10-minute TTL triggers periodic re-fetches. Failed lookups fall back to stale cached names. + +## Self-telemetry + +The worker emits its own health metrics alongside the Cloudflare data: + +- `cloudflare_metrics_cron_summary` — datasets/points/errors per tick +- `cloudflare_metrics_cron_error{reason}` — early-exit errors +- `cloudflare_metrics_collector_dataset{dataset,status}` — per-dataset rows/points/duration/errors +- `cloudflare_metrics_resource_lookup{resource,status}` — REST lookup outcomes +- `cloudflare_metrics_graphql_client` — requests/error_responses per tick +- `cloudflare_metrics_flush{status}` — bytes/duration/pending buffers (from previous tick) +- `cloudflare_metrics_http_response{method,path,status}` — HTTP handler counts +- `cloudflare_metrics_handle_request` — handler duration/invocation + +## Dashboards + +20 Grafana dashboards managed via Terraform, in `deployment/.../dashboards/`: + +| Dashboard | Template variables | +| ---------------------------------------------- | ------------------------------- | +| Account Overview | — | +| Workers | `$script_name`, `$status` | +| Workers Scheduled | `$script_name`, `$cron` | +| D1 | `$database_name` | +| R2 | `$bucket_name` | +| KV | `$namespace_id` | +| Durable Objects | `$script_name`, `$namespace_id` | +| Queues | `$queue_name` | +| Hyperdrive | `$config_id` | +| HTTP / Zones | `$zone_name` | +| Pages Functions | `$script_name` | +| AI, Vectorize, Browser, Stream, RUM, Pipelines | — | +| DNS | `$zone_name` | +| Email | `$zone_name` | +| Exporter Health | — | + +## Alerts + +7 Grafana alert rules in `deployment/.../alerts.tf`, all linked to the exporter-health dashboard: + +| Rule | Condition | Severity | +| ---------------------------------- | ---------------------------------- | -------- | +| Collector Not Running | No `cron_summary_datasets` for 10m | 1 | +| Collector Cron Error | `cron_error_count > 0` in 10m | 1 | +| Dataset Errors Sustained | `>10` errors in 15m | 3 | +| Metrics Flush Failing | `>3` flush errors in 15m | 1 | +| Pending Flush Buffer Growing | `>3` stashed bodies for 10m | 3 | +| GraphQL Subrequest Budget Near Cap | `>40` requests/tick for 10m | 3 | +| GraphQL Error Responses Sustained | `>5` error responses in 15m | 3 | + +## File structure + +``` +src/ + index.ts Entry point (wires handlers) + handlers/ + http.ts /health endpoint + scheduled.ts Cron handler (collect + flush) + collector.ts Orchestration (collectAll → batched account/zone fetches) + resource-cache.ts REST resource lookups + module-level caching + emit.ts DatasetRow → Metric translation + tag enrichment + graphql-client.ts CloudflareGraphQLClient (HTTP transport + chunking) + graphql-builders.ts Query construction (pure functions) + cloudflare-api.ts CloudflareRestClient (D1/queues/zones REST) + metrics.ts CloudflareMetricsRepository facade + metric.ts Metric data class + metric-providers.ts InfluxMetricsProvider + HeaderMetricsProvider + flush-state.ts Retry buffer + last-flush stats (module-level state) + datasets.ts 78 DatasetQuery definitions + types.ts Shared types + deferred.ts DeferredRepository (waitUntil helper) + monitor.ts monitorAsyncFunction (duration/invocation wrapper) +``` + +## Development + +```bash +pnpm run dev # wrangler dev (local) +pnpm run test # 71 unit tests +pnpm run test:integration # live API tests (needs CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID) +pnpm run check # tsc --noEmit +pnpm run build # wrangler deploy --dry-run +``` + +Local `.dev.vars`: + +``` +CLOUDFLARE_API_TOKEN=... +CLOUDFLARE_ACCOUNT_ID=... +VMETRICS_API_TOKEN=... +ENVIRONMENT=dev +``` + +## Infrastructure + +- **Worker**: `apps/cloudflare-metrics/` — TypeScript, Wrangler, every-minute cron +- **Terraform**: `deployment/modules/cloudflare/workers/cloudflare-metrics/` — worker, version, deployment, cron trigger, dashboards, alerts, and a scoped API token with Account Analytics Read + D1 Read + Queues Read + Zone Read +- **CI**: unit tests on every PR, path-filtered integration tests (gated on 1Password credentials), build + deploy-dev on PR, deploy-prod on merge to main diff --git a/apps/cloudflare-metrics/package.json b/apps/cloudflare-metrics/package.json new file mode 100644 index 0000000..4b5ab42 --- /dev/null +++ b/apps/cloudflare-metrics/package.json @@ -0,0 +1,17 @@ +{ + "name": "@immich-services/cloudflare-metrics", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "build": "wrangler deploy --dry-run --outdir ../../dist/cloudflare-metrics", + "tail": "wrangler tail", + "test": "vitest run --exclude 'src/integration.test.ts'", + "test:integration": "vitest run --config vitest.integration.config.ts", + "check": "tsc --noEmit" + }, + "dependencies": { + "@influxdata/influxdb-client": "^1.34.0" + } +} diff --git a/apps/cloudflare-metrics/src/cloudflare-api.test.ts b/apps/cloudflare-metrics/src/cloudflare-api.test.ts new file mode 100644 index 0000000..efb89f4 --- /dev/null +++ b/apps/cloudflare-metrics/src/cloudflare-api.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, vi } from 'vitest'; +import { CloudflareRestClient, CloudflareRestError } from './cloudflare-api.js'; + +describe('CloudflareRestClient', () => { + it('paginates through listD1Databases', async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + success: true, + result: [{ uuid: 'a', name: 'one' }], + result_info: { page: 1, per_page: 100, total_pages: 2, count: 1, total_count: 2 }, + }), + { status: 200 }, + ), + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + success: true, + result: [{ uuid: 'b', name: 'two' }], + result_info: { page: 2, per_page: 100, total_pages: 2, count: 1, total_count: 2 }, + }), + { status: 200 }, + ), + ); + + const client = new CloudflareRestClient('tok', 'https://example.com', fetchMock as unknown as typeof fetch); + const dbs = await client.listD1Databases('acct'); + expect(dbs).toEqual([ + { uuid: 'a', name: 'one' }, + { uuid: 'b', name: 'two' }, + ]); + expect(fetchMock).toHaveBeenCalledTimes(2); + const firstCall = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; + expect(firstCall[0]).toContain('/accounts/acct/d1/database?page=1'); + expect((firstCall[1].headers as Record).Authorization).toBe('Bearer tok'); + }); + + it('throws CloudflareRestError on non-OK responses', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response('nope', { status: 403 })); + const client = new CloudflareRestClient('tok', 'https://example.com', fetchMock as unknown as typeof fetch); + await expect(client.listQueues('acct')).rejects.toBeInstanceOf(CloudflareRestError); + }); + + it('keeps the existing query string when listing zones', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + result: [{ id: 'z1', name: 'example.com' }], + result_info: { page: 1, per_page: 100, total_pages: 1, count: 1, total_count: 1 }, + }), + { status: 200 }, + ), + ); + const client = new CloudflareRestClient('tok', 'https://example.com', fetchMock as unknown as typeof fetch); + await client.listZones('acct'); + const call = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; + expect(call[0]).toContain('/zones?account.id=acct&page=1'); + }); + + it('returns null from getZone when the API responds with 404', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response('', { status: 404 })); + const client = new CloudflareRestClient('tok', 'https://example.com', fetchMock as unknown as typeof fetch); + const zone = await client.getZone('does-not-exist'); + expect(zone).toBeNull(); + }); + + it('returns the zone payload from getZone on success', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + result: { id: 'z1', name: 'pages.example.com' }, + }), + { status: 200 }, + ), + ); + const client = new CloudflareRestClient('tok', 'https://example.com', fetchMock as unknown as typeof fetch); + const zone = await client.getZone('z1'); + expect(zone).toEqual({ id: 'z1', name: 'pages.example.com' }); + const call = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; + expect(call[0]).toBe('https://example.com/zones/z1'); + }); +}); diff --git a/apps/cloudflare-metrics/src/cloudflare-api.ts b/apps/cloudflare-metrics/src/cloudflare-api.ts new file mode 100644 index 0000000..8d247f3 --- /dev/null +++ b/apps/cloudflare-metrics/src/cloudflare-api.ts @@ -0,0 +1,118 @@ +const DEFAULT_BASE_URL = 'https://api.cloudflare.com/client/v4'; +const DEFAULT_PAGE_SIZE = 100; + +export interface D1Database { + uuid: string; + name: string; +} + +export interface Queue { + queue_id: string; + queue_name: string; +} + +export interface Zone { + id: string; + name: string; +} + +interface CloudflareListResponse { + result: T[] | null; + result_info?: { + page: number; + per_page: number; + total_pages: number; + count: number; + total_count: number; + }; + success: boolean; + errors?: Array<{ code: number; message: string }>; +} + +export class CloudflareRestError extends Error { + constructor( + message: string, + public readonly statusCode: number, + ) { + super(message); + this.name = 'CloudflareRestError'; + } +} + +export interface ICloudflareRestClient { + listD1Databases(accountId: string): Promise; + listQueues(accountId: string): Promise; + listZones(accountId: string): Promise; + getZone(zoneId: string): Promise; +} + +export class CloudflareRestClient implements ICloudflareRestClient { + constructor( + private readonly apiToken: string, + private readonly baseUrl: string = DEFAULT_BASE_URL, + private readonly fetchImpl?: typeof fetch, + ) {} + + async listD1Databases(accountId: string): Promise { + return this.listPaginated(`/accounts/${encodeURIComponent(accountId)}/d1/database`); + } + + async listQueues(accountId: string): Promise { + return this.listPaginated(`/accounts/${encodeURIComponent(accountId)}/queues`); + } + + async listZones(accountId: string): Promise { + return this.listPaginated(`/zones?account.id=${encodeURIComponent(accountId)}`); + } + + async getZone(zoneId: string): Promise { + const doFetch = this.fetchImpl ?? globalThis.fetch; + const url = `${this.baseUrl}/zones/${encodeURIComponent(zoneId)}`; + const response = await doFetch(url, { + headers: { Authorization: `Bearer ${this.apiToken}` }, + }); + if (response.status === 404) { + await response.body?.cancel(); + return null; + } + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new CloudflareRestError( + `Cloudflare REST error (${response.status}) for /zones/${zoneId}: ${body.slice(0, 200)}`, + response.status, + ); + } + const payload = (await response.json()) as { result: Zone | null }; + return payload.result ?? null; + } + + private async listPaginated(path: string): Promise { + const all: T[] = []; + let page = 1; + const separator = path.includes('?') ? '&' : '?'; + const doFetch = this.fetchImpl ?? globalThis.fetch; + for (;;) { + const url = `${this.baseUrl}${path}${separator}page=${page}&per_page=${DEFAULT_PAGE_SIZE}`; + const response = await doFetch(url, { + headers: { Authorization: `Bearer ${this.apiToken}` }, + }); + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new CloudflareRestError( + `Cloudflare REST error (${response.status}) for ${path}: ${body.slice(0, 200)}`, + response.status, + ); + } + const payload = (await response.json()) as CloudflareListResponse; + if (payload.result) { + all.push(...payload.result); + } + const info = payload.result_info; + if (!info || page >= (info.total_pages ?? 1)) { + break; + } + page++; + } + return all; + } +} diff --git a/apps/cloudflare-metrics/src/collector.test.ts b/apps/cloudflare-metrics/src/collector.test.ts new file mode 100644 index 0000000..b618738 --- /dev/null +++ b/apps/cloudflare-metrics/src/collector.test.ts @@ -0,0 +1,658 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { CloudflareMetricsCollector } from './collector.js'; +import { + D1_QUERIES, + DURABLE_OBJECTS_PERIODIC, + DURABLE_OBJECTS_STORAGE, + HTTP_REQUESTS_OVERVIEW, + HYPERDRIVE_POOL, + QUEUE_BACKLOG, + QUEUE_OPERATIONS, + R2_OPERATIONS, + WORKERS_INVOCATIONS, +} from './datasets.js'; +import type { CloudflareGraphQLClient } from './graphql-client.js'; +import { type CloudflareMetricsRepository } from './metrics.js'; +import { __resetResourceCachesForTests } from './resource-cache.js'; +import { + asGraphQLClient, + buildMetricsRepo, + FakeGraphQLClient, + FakeRestClient, + RecordingProvider, + ThrowingRestClient, +} from './test-helpers.js'; +import type { DatasetQuery, DatasetRow } from './types.js'; + +describe('CloudflareMetricsCollector', () => { + let provider: RecordingProvider; + let metrics: CloudflareMetricsRepository; + + beforeEach(() => { + provider = new RecordingProvider(); + metrics = buildMetricsRepo(provider); + __resetResourceCachesForTests(); + }); + + it('computes a lagged query range so late-arriving buckets are included', () => { + const now = new Date('2026-04-10T12:00:00Z'); + const collector = new CloudflareMetricsCollector(asGraphQLClient(new FakeGraphQLClient({})), 'acct', metrics, { + now: () => now, + lagMs: 2 * 60 * 1000, + windowMs: 5 * 60 * 1000, + }); + const range = collector.getRange(); + expect(range.end).toEqual(new Date('2026-04-10T11:58:00Z')); + expect(range.start).toEqual(new Date('2026-04-10T11:53:00Z')); + }); + + it('emits a workers invocations metric with snake_case tags and fields, scaled durations', async () => { + const row: DatasetRow = { + dimensions: { + datetimeMinute: '2026-04-10T12:00:00Z', + scriptName: 'version-api-prod', + status: 'success', + scriptVersion: 'abc', + usageModel: 'standard', + }, + sum: { + requests: 9, + errors: 0, + subrequests: 9, + clientDisconnects: 0, + cpuTimeUs: 7474, + duration: 0.165_879, + wallTime: 1_327_032, + responseBodySize: 522, + }, + max: { + cpuTime: 1400, + duration: 0.037_329_5, + wallTime: 298_636, + requestDuration: 1579, + responseBodySize: 58, + }, + quantiles: { + cpuTimeP50: 730, + cpuTimeP99: 1400, + durationP50: 0.015_286_75, + durationP99: 0.037_329_5, + wallTimeP50: 122_294, + wallTimeP99: 298_636, + responseBodySizeP99: 58, + }, + }; + + const client = new FakeGraphQLClient({ workers_invocations: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct-xyz', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + + const [result] = await collector.collectAll([WORKERS_INVOCATIONS]); + expect(result.points).toBe(1); + expect(result.rows).toBe(1); + expect(result.error).toBeUndefined(); + + const exported = provider.metrics.find((m) => m.name === 'cf_workers_invocations'); + expect(exported).toBeDefined(); + expect(exported?.tags.get('script_name')).toBe('version-api-prod'); + expect(exported?.tags.get('status')).toBe('success'); + expect(exported?.tags.get('account_id')).toBe('acct-xyz'); + expect(exported?.exportTimestamp).toEqual(new Date('2026-04-10T12:00:00Z')); + expect(exported?.fields.get('requests')).toEqual({ value: 9, type: 'int' }); + // Duration in seconds (0.165879) × 1000 = 165.879 ms, stored as float + expect(exported?.fields.get('duration_ms_sum')).toEqual({ value: 165.879, type: 'float' }); + expect(exported?.fields.get('duration_ms_p99')).toEqual({ value: 37.3295, type: 'float' }); + expect(exported?.fields.get('cpu_time_us_p99')).toEqual({ value: 1400, type: 'int' }); + }); + + it('coerces numeric dimensions to string tags', async () => { + const row: DatasetRow = { + dimensions: { + datetimeMinute: '2026-04-10T12:00:00Z', + bucketName: 'my-bucket', + actionType: 'GetObject', + actionStatus: 'success', + responseStatusCode: 200, + storageClass: 'Standard', + }, + sum: { requests: 5, responseBytes: 100, responseObjectSize: 100 }, + }; + const client = new FakeGraphQLClient({ r2_operations: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([R2_OPERATIONS]); + const exported = provider.metrics.find((m) => m.name === 'cf_r2_operations'); + expect(exported?.tags.get('response_status_code')).toBe('200'); + expect(exported?.tags.get('bucket_name')).toBe('my-bucket'); + }); + + it('drops rows with neither a timestamp dimension nor any numeric field', async () => { + const row: DatasetRow = { dimensions: { scriptName: 'no-timestamp' }, sum: { readQueries: 1 } }; + const client = new FakeGraphQLClient({ d1_queries: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + const [result] = await collector.collectAll([D1_QUERIES]); + expect(result.rows).toBe(1); + expect(result.points).toBe(0); + expect(provider.metrics.some((m) => m.name === 'cf_d1_queries')).toBe(false); + }); + + it('skips null-valued fields rather than emitting zeros', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', databaseId: 'db1', databaseRole: 'primary' }, + sum: { readQueries: null, writeQueries: 0, rowsRead: 10, rowsWritten: null, queryBatchResponseBytes: 100 }, + avg: { queryBatchTimeMs: null, queryBatchResponseBytes: 100, sampleInterval: 1 }, + }; + const client = new FakeGraphQLClient({ d1_queries: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([D1_QUERIES]); + const exported = provider.metrics.find((m) => m.name === 'cf_d1_queries'); + expect(exported?.fields.has('read_queries')).toBe(false); + expect(exported?.fields.has('rows_written')).toBe(false); + expect(exported?.fields.get('write_queries')).toEqual({ value: 0, type: 'int' }); + expect(exported?.fields.get('rows_read')).toEqual({ value: 10, type: 'int' }); + expect(exported?.fields.get('query_duration_ms_avg')).toBeUndefined(); + expect(exported?.fields.get('sample_interval')).toEqual({ value: 1, type: 'float' }); + }); + + it('records a per-dataset collector metric tagged with success/error', async () => { + const client = new FakeGraphQLClient({ workers_invocations: [] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([WORKERS_INVOCATIONS]); + const observed = provider.metrics.find( + (m) => m.name === 'cloudflare_metrics_collector_dataset' && m.tags.get('dataset') === 'workers_invocations', + ); + expect(observed?.tags.get('status')).toBe('success'); + expect(observed?.fields.get('rows')).toEqual({ value: 0, type: 'int' }); + }); + + it('reports per-dataset errors from a batch response without aborting others', async () => { + // Simulates a partial GraphQL response where one field is rejected and + // the others still return data. The batched client surfaces the error + // in `batchResult.errors[dataset.key]`, the collector maps it to a + // collector_dataset{status=error} self-metric, and the other datasets + // still get emitted. + class PartialErrorClient { + // eslint-disable-next-line @typescript-eslint/require-await + async fetchAccountBatch( + _accountTag: string, + datasets: readonly DatasetQuery[], + _range: { start: Date; end: Date }, + options: { includeScheduledInvocations?: boolean } = {}, + ) { + const rows: Record = {}; + const errors: Record = {}; + for (const dataset of datasets) { + if (dataset.key === 'workers_invocations') { + errors[dataset.key] = 'boom'; + } else { + rows[dataset.key] = []; + } + } + return { + rows, + errors, + scheduledInvocations: options.includeScheduledInvocations ? [] : undefined, + }; + } + // eslint-disable-next-line @typescript-eslint/require-await + async fetchZoneBatch() { + return { rows: {}, errors: {} }; + } + } + const collector = new CloudflareMetricsCollector( + new PartialErrorClient() as unknown as CloudflareGraphQLClient, + 'acct', + metrics, + { now: () => new Date('2026-04-10T12:15:00Z') }, + ); + + const results = await collector.collectAll([WORKERS_INVOCATIONS, R2_OPERATIONS]); + const workersResult = results.find((r) => r.dataset === 'workers_invocations'); + const r2Result = results.find((r) => r.dataset === 'r2_operations'); + expect(workersResult?.error).toContain('boom'); + expect(r2Result?.error).toBeUndefined(); + const errorMetric = provider.metrics.find( + (m) => m.tags.get('dataset') === 'workers_invocations' && m.tags.get('status') === 'error', + ); + expect(errorMetric?.tags.get('error')).toBe('graphql_field_error'); + expect(errorMetric?.tags.get('error_scope')).toBe('field'); + expect(errorMetric?.tags.get('error_message')).toBe('boom'); + }); + + it('accepts date-granularity datasets and synthesizes a midnight UTC timestamp', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, + max: { storedBytes: 1024 }, + }; + const client = new FakeGraphQLClient({ durable_objects_storage: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([DURABLE_OBJECTS_STORAGE]); + const exported = provider.metrics.find((m) => m.name === 'cf_durable_objects_storage'); + expect(exported?.exportTimestamp).toEqual(new Date('2026-04-10T12:00:00Z')); + expect(exported?.fields.get('stored_bytes')).toEqual({ value: 1024, type: 'int' }); + }); + + it('emits periodic durable-object fields with correct unit scaling', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', namespaceId: 'ns' }, + sum: { + activeTime: 1000, + cpuTime: 500, + duration: 72.99, + exceededCpuErrors: 0, + exceededMemoryErrors: 0, + fatalInternalErrors: 2, + inboundWebsocketMsgCount: 0, + outboundWebsocketMsgCount: 10, + rowsRead: 100, + rowsWritten: 50, + storageDeletes: 0, + storageReadUnits: 0, + storageWriteUnits: 0, + subrequests: 0, + }, + max: { activeWebsocketConnections: 5 }, + }; + const client = new FakeGraphQLClient({ durable_objects_periodic: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([DURABLE_OBJECTS_PERIODIC]); + const exported = provider.metrics.find((m) => m.name === 'cf_durable_objects_periodic'); + expect(exported?.fields.get('duration_ms')).toEqual({ value: 72_990, type: 'float' }); + expect(exported?.fields.get('fatal_internal_errors')).toEqual({ value: 2, type: 'int' }); + expect(exported?.fields.get('active_ws_connections_max')).toEqual({ value: 5, type: 'int' }); + }); + + it('handles avg-only datasets like queue backlog', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', queueId: 'q1' }, + avg: { bytes: 123.4, messages: 7.5, sampleInterval: 1 }, + }; + const client = new FakeGraphQLClient({ queue_backlog: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([QUEUE_BACKLOG]); + const exported = provider.metrics.find((m) => m.name === 'cf_queue_backlog'); + expect(exported?.fields.get('backlog_bytes_avg')).toEqual({ value: 123.4, type: 'float' }); + expect(exported?.fields.get('backlog_messages_avg')).toEqual({ value: 7.5, type: 'float' }); + }); + + it('uses datetimeMinute timestamp dimension for hyperdrive pool sizes', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', configId: 'cfg', databaseType: 'pg' }, + max: { currentPoolSize: 3, maxPoolSize: 10, waitingClients: 0 }, + }; + const client = new FakeGraphQLClient({ hyperdrive_pool: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([HYPERDRIVE_POOL]); + const exported = provider.metrics.find((m) => m.name === 'cf_hyperdrive_pool'); + expect(exported?.exportTimestamp).toEqual(new Date('2026-04-10T12:00:00Z')); + expect(exported?.fields.get('current_pool_size')).toEqual({ value: 3, type: 'int' }); + }); + + it('enriches D1 metrics with database_name from the rest client', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', databaseId: 'db-123', databaseRole: 'primary' }, + sum: { readQueries: 10, writeQueries: 0, rowsRead: 100, rowsWritten: 0, queryBatchResponseBytes: 200 }, + }; + const client = new FakeGraphQLClient({ d1_queries: [row] }); + const restClient = new FakeRestClient([{ uuid: 'db-123', name: 'releases-prod' }]); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([D1_QUERIES]); + const exported = provider.metrics.find((m) => m.name === 'cf_d1_queries'); + expect(exported?.tags.get('database_id')).toBe('db-123'); + expect(exported?.tags.get('database_name')).toBe('releases-prod'); + }); + + it('enriches queue metrics with queue_name from the rest client', async () => { + const row: DatasetRow = { + dimensions: { + datetimeMinute: '2026-04-10T12:00:00Z', + queueId: 'q-abc', + actionType: 'WriteMessage', + consumerType: 'worker', + outcome: 'success', + }, + sum: { billableOperations: 5, bytes: 1234 }, + }; + const client = new FakeGraphQLClient({ queue_operations: [row] }); + const restClient = new FakeRestClient([], [{ queue_id: 'q-abc', queue_name: 'ingest-events' }]); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([QUEUE_OPERATIONS]); + const exported = provider.metrics.find((m) => m.name === 'cf_queue_operations'); + expect(exported?.tags.get('queue_id')).toBe('q-abc'); + expect(exported?.tags.get('queue_name')).toBe('ingest-events'); + }); + + it('enriches queue backlog metrics with queue_name', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', queueId: 'q-abc' }, + avg: { bytes: 100, messages: 2, sampleInterval: 1 }, + }; + const client = new FakeGraphQLClient({ queue_backlog: [row] }); + const restClient = new FakeRestClient([], [{ queue_id: 'q-abc', queue_name: 'ingest-events' }]); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([QUEUE_BACKLOG]); + const exported = provider.metrics.find((m) => m.name === 'cf_queue_backlog'); + expect(exported?.tags.get('queue_name')).toBe('ingest-events'); + }); + + it('enriches HTTP overview metrics with zone_name', async () => { + const row: DatasetRow = { + dimensions: { + datetimeMinute: '2026-04-10T12:00:00Z', + zoneTag: 'zone-1', + clientCountryName: 'US', + clientRequestHTTPProtocol: 'HTTP/2', + edgeResponseStatus: 200, + }, + sum: { requests: 100, bytes: 5000, cachedRequests: 50, cachedBytes: 2500, pageViews: 0, visits: 0 }, + }; + const client = new FakeGraphQLClient({ http_requests_overview: [row] }); + const restClient = new FakeRestClient([], [], [{ id: 'zone-1', name: 'example.com' }]); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([HTTP_REQUESTS_OVERVIEW]); + const exported = provider.metrics.find((m) => m.name === 'cf_http_requests_overview'); + expect(exported?.tags.get('zone_tag')).toBe('zone-1'); + expect(exported?.tags.get('zone_name')).toBe('example.com'); + }); + + it('resolves HTTP zones missing from the bulk list via per-tag lookup', async () => { + const bulkZone: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', zoneTag: 'zone-bulk' }, + sum: { requests: 10, bytes: 500, cachedRequests: 0, cachedBytes: 0, pageViews: 0, visits: 0 }, + }; + const pagesZone: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', zoneTag: 'zone-pages' }, + sum: { requests: 5, bytes: 200, cachedRequests: 0, cachedBytes: 0, pageViews: 0, visits: 0 }, + }; + const client = new FakeGraphQLClient({ http_requests_overview: [bulkZone, pagesZone] }); + const restClient = new FakeRestClient([], [], [{ id: 'zone-bulk', name: 'bulk.example.com' }], { + 'zone-pages': { id: 'zone-pages', name: 'pages.example.com' }, + }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([HTTP_REQUESTS_OVERVIEW]); + + const exported = provider.metrics.filter((m) => m.name === 'cf_http_requests_overview'); + const bulk = exported.find((m) => m.tags.get('zone_tag') === 'zone-bulk'); + const pages = exported.find((m) => m.tags.get('zone_tag') === 'zone-pages'); + expect(bulk?.tags.get('zone_name')).toBe('bulk.example.com'); + expect(pages?.tags.get('zone_name')).toBe('pages.example.com'); + + // Only the uncached zone should have been looked up individually. + expect(restClient.getZoneCalls).toEqual(['zone-pages']); + + const lookup = provider.metrics.find( + (m) => m.name === 'cloudflare_metrics_resource_lookup' && m.tags.get('resource') === 'zones_individual', + ); + expect(lookup?.tags.get('status')).toBe('success'); + expect(lookup?.fields.get('resolved')).toEqual({ value: 1, type: 'int' }); + expect(lookup?.fields.get('failed')).toEqual({ value: 0, type: 'int' }); + }); + + it('falls back to the zoneTag when individual lookup returns nothing', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', zoneTag: 'zone-unknown' }, + sum: { requests: 1, bytes: 1, cachedRequests: 0, cachedBytes: 0, pageViews: 0, visits: 0 }, + }; + const client = new FakeGraphQLClient({ http_requests_overview: [row] }); + // No bulk zones and no individual lookup results — rest client returns null. + const restClient = new FakeRestClient([], [], [], { 'zone-unknown': null }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([HTTP_REQUESTS_OVERVIEW]); + + const exported = provider.metrics.find((m) => m.name === 'cf_http_requests_overview'); + expect(exported?.tags.get('zone_tag')).toBe('zone-unknown'); + expect(exported?.tags.get('zone_name')).toBe('zone-unknown'); + }); + + it('omits the enrichment tag when the rest client has no match', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', databaseId: 'db-unknown', databaseRole: 'primary' }, + sum: { readQueries: 1, writeQueries: 0, rowsRead: 0, rowsWritten: 0, queryBatchResponseBytes: 0 }, + }; + const client = new FakeGraphQLClient({ d1_queries: [row] }); + const restClient = new FakeRestClient([{ uuid: 'db-123', name: 'releases-prod' }]); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([D1_QUERIES]); + const exported = provider.metrics.find((m) => m.name === 'cf_d1_queries'); + expect(exported?.tags.get('database_id')).toBe('db-unknown'); + expect(exported?.tags.has('database_name')).toBe(false); + }); + + it('records per-resource lookup self-metrics with success and error statuses', async () => { + const client = new FakeGraphQLClient({}); + const restClient = new ThrowingRestClient(); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + await collector.collectAll([]); + const lookups = provider.metrics.filter((m) => m.name === 'cloudflare_metrics_resource_lookup'); + expect(lookups).toHaveLength(3); + for (const lookup of lookups) { + expect(lookup.tags.get('status')).toBe('error'); + } + const resources = new Set(lookups.map((m) => m.tags.get('resource'))); + expect(resources).toEqual(new Set(['d1_databases', 'queues', 'zones'])); + }); + + it('falls back to unenriched metrics when no rest client is provided', async () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', databaseId: 'db-123', databaseRole: 'primary' }, + sum: { readQueries: 1, writeQueries: 0, rowsRead: 0, rowsWritten: 0, queryBatchResponseBytes: 0 }, + }; + const client = new FakeGraphQLClient({ d1_queries: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([D1_QUERIES]); + const exported = provider.metrics.find((m) => m.name === 'cf_d1_queries'); + expect(exported?.tags.has('database_name')).toBe(false); + expect(provider.metrics.some((m) => m.name === 'cloudflare_metrics_resource_lookup')).toBe(false); + }); + + it('iterates cached zones for zone-scoped datasets and injects zone tags', async () => { + const zoneScoped: DatasetQuery = { + key: 'zone_detail', + measurement: 'cf_zone_detail', + field: 'httpRequestsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'edgeResponseStatus'], + topLevelFields: ['count'], + blocks: { sum: ['edgeResponseBytes'] }, + tags: [{ source: 'edgeResponseStatus', as: 'edge_response_status' }], + fields: { + requests: { type: 'int', source: ['_top', 'count'] }, + edge_response_bytes: { type: 'int', source: ['sum', 'edgeResponseBytes'] }, + }, + }; + const client = new FakeGraphQLClient( + {}, + { + zone_detail: { + 'zone-a': [ + { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', edgeResponseStatus: 200 }, + count: 42, + sum: { edgeResponseBytes: 1024 }, + }, + ], + 'zone-b': [ + { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', edgeResponseStatus: 500 }, + count: 3, + sum: { edgeResponseBytes: 0 }, + }, + ], + }, + }, + ); + const restClient = new FakeRestClient( + [], + [], + [ + { id: 'zone-a', name: 'a.example.com' }, + { id: 'zone-b', name: 'b.example.com' }, + ], + ); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + restClient, + }); + + const results = await collector.collectAll([zoneScoped]); + const zoneResult = results.find((r) => r.dataset === 'zone_detail'); + expect(zoneResult?.rows).toBe(2); + expect(zoneResult?.points).toBe(2); + expect(client.zoneCalls).toEqual([{ zoneTags: ['zone-a', 'zone-b'], dataset: 'zone_detail' }]); + + const exported = provider.metrics.filter((m) => m.name === 'cf_zone_detail'); + expect(exported).toHaveLength(2); + const a = exported.find((m) => m.tags.get('zone_tag') === 'zone-a'); + const b = exported.find((m) => m.tags.get('zone_tag') === 'zone-b'); + expect(a?.tags.get('zone_name')).toBe('a.example.com'); + expect(a?.fields.get('requests')).toEqual({ value: 42, type: 'int' }); + expect(b?.tags.get('zone_name')).toBe('b.example.com'); + expect(b?.fields.get('edge_response_bytes')).toEqual({ value: 0, type: 'int' }); + }); + + it('skips zone-scoped datasets when the zone cache is empty', async () => { + const zoneScoped: DatasetQuery = { + key: 'zone_detail', + measurement: 'cf_zone_detail', + field: 'httpRequestsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute'], + topLevelFields: ['count'], + blocks: {}, + tags: [], + fields: { requests: { type: 'int', source: ['_top', 'count'] } }, + }; + const client = new FakeGraphQLClient({}); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + const [result] = await collector.collectAll([zoneScoped]); + expect(result.rows).toBe(0); + expect(result.points).toBe(0); + const observed = provider.metrics.find( + (m) => m.name === 'cloudflare_metrics_collector_dataset' && m.tags.get('dataset') === 'zone_detail', + ); + expect(observed?.tags.get('status')).toBe('skipped'); + expect(observed?.tags.get('reason')).toBe('no_zones_in_cache'); + }); + + it('aggregates scheduled worker invocations client-side into minute buckets', async () => { + const client = new FakeGraphQLClient({}, {}, [ + { + scriptName: 'version-api', + cron: '*/5 * * * *', + status: 'success', + datetime: '2026-04-10T12:00:10Z', + cpuTimeUs: 1000, + }, + { + scriptName: 'version-api', + cron: '*/5 * * * *', + status: 'success', + datetime: '2026-04-10T12:00:42Z', + cpuTimeUs: 3000, + }, + { + scriptName: 'version-api', + cron: '*/5 * * * *', + status: 'error', + datetime: '2026-04-10T12:01:12Z', + cpuTimeUs: 500, + }, + ]); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + const results = await collector.collectAll([]); + const scheduledResult = results.find((r) => r.dataset === 'workers_scheduled'); + expect(scheduledResult?.rows).toBe(3); + expect(scheduledResult?.points).toBe(2); // one bucket for success in 12:00, one for error in 12:01 + + const scheduled = provider.metrics.filter((m) => m.name === 'cf_workers_scheduled'); + expect(scheduled).toHaveLength(2); + + const successBucket = scheduled.find((m) => m.tags.get('status') === 'success'); + expect(successBucket?.fields.get('invocations')).toEqual({ value: 2, type: 'int' }); + expect(successBucket?.fields.get('cpu_time_us_sum')).toEqual({ value: 4000, type: 'int' }); + expect(successBucket?.fields.get('cpu_time_us_avg')).toEqual({ value: 2000, type: 'int' }); + expect(successBucket?.fields.get('cpu_time_us_max')).toEqual({ value: 3000, type: 'int' }); + expect(successBucket?.tags.get('script_name')).toBe('version-api'); + expect(successBucket?.tags.get('cron')).toBe('*/5 * * * *'); + expect(successBucket?.exportTimestamp).toEqual(new Date('2026-04-10T12:00:00Z')); + + const errorBucket = scheduled.find((m) => m.tags.get('status') === 'error'); + expect(errorBucket?.fields.get('invocations')).toEqual({ value: 1, type: 'int' }); + expect(errorBucket?.exportTimestamp).toEqual(new Date('2026-04-10T12:01:00Z')); + }); + + it('reads top-level count fields from the row', async () => { + const dataset: DatasetQuery = { + key: 'd1_queries_detail', + measurement: 'cf_d1_queries_detail', + field: 'd1QueriesAdaptiveGroups', + dimensions: ['datetimeMinute', 'databaseId'], + topLevelFields: ['count'], + blocks: { sum: ['queryDurationMs'] }, + tags: [{ source: 'databaseId', as: 'database_id' }], + fields: { + query_count: { type: 'int', source: ['_top', 'count'] }, + query_duration_ms_sum: { type: 'float', source: ['sum', 'queryDurationMs'] }, + }, + }; + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', databaseId: 'db-1' }, + count: 42, + sum: { queryDurationMs: 7.5 }, + }; + const client = new FakeGraphQLClient({ d1_queries_detail: [row] }); + const collector = new CloudflareMetricsCollector(asGraphQLClient(client), 'acct', metrics, { + now: () => new Date('2026-04-10T12:15:00Z'), + }); + await collector.collectAll([dataset]); + const exported = provider.metrics.find((m) => m.name === 'cf_d1_queries_detail'); + expect(exported?.fields.get('query_count')).toEqual({ value: 42, type: 'int' }); + expect(exported?.fields.get('query_duration_ms_sum')).toEqual({ value: 7.5, type: 'float' }); + }); +}); diff --git a/apps/cloudflare-metrics/src/collector.ts b/apps/cloudflare-metrics/src/collector.ts new file mode 100644 index 0000000..e1dd6fc --- /dev/null +++ b/apps/cloudflare-metrics/src/collector.ts @@ -0,0 +1,407 @@ +import type { ICloudflareRestClient } from './cloudflare-api.js'; +import { buildMetric, normalizeTagValue } from './emit.js'; +import { CloudflareGraphQLError, type ICloudflareGraphQLClient } from './graphql-client.js'; +import { Metric } from './metric.js'; +import type { CloudflareMetricsRepository } from './metrics.js'; +import { ResourceCacheService } from './resource-cache.js'; +import type { CollectionResult, DatasetQuery, DatasetRow } from './types.js'; + +export interface CollectorOptions { + /** Cloudflare analytics data lags by a few minutes; this offsets the window end. */ + lagMs?: number; + windowMs?: number; + now?: () => Date; + restClient?: ICloudflareRestClient; +} + +// 5m lag avoids querying buckets Cloudflare hasn't finalised yet (pipeline +// delay is typically 2-5m). 5m window gives us ~5 consecutive ticks worth +// of overlap so a missed minute is still covered by neighbouring ticks. +// Cloudflare drops/delays ~25% of our cron triggers in practice; a wider +// window makes multi-minute gaps less likely to turn into data loss. +// VictoriaMetrics dedupes the overlap so there's no storage cost. +const DEFAULT_LAG_MS = 5 * 60 * 1000; +const DEFAULT_WINDOW_MS = 5 * 60 * 1000; + +export class CloudflareMetricsCollector { + private readonly lagMs: number; + private readonly windowMs: number; + private readonly now: () => Date; + private readonly resourceCache: ResourceCacheService; + + constructor( + private readonly client: ICloudflareGraphQLClient, + private readonly accountTag: string, + private readonly metrics: CloudflareMetricsRepository, + options: CollectorOptions = {}, + ) { + this.lagMs = options.lagMs ?? DEFAULT_LAG_MS; + this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS; + this.now = options.now ?? (() => new Date()); + this.resourceCache = new ResourceCacheService(this.accountTag, this.metrics, this.now, options.restClient); + } + + getRange(): { start: Date; end: Date } { + const end = new Date(this.now().getTime() - this.lagMs); + const start = new Date(end.getTime() - this.windowMs); + return { start, end }; + } + + async collectAll(datasets: readonly DatasetQuery[]): Promise { + await this.resourceCache.populate(); + const range = this.getRange(); + const results: CollectionResult[] = []; + + // Single-pass classification — avoids two .filter() allocations over the + // ~79-dataset registry on every tick. + const accountDatasets: DatasetQuery[] = []; + const zoneDatasets: DatasetQuery[] = []; + for (const d of datasets) { + if (d.scope === 'zone') { + zoneDatasets.push(d); + } else { + accountDatasets.push(d); + } + } + + results.push(...(await this.collectAccountBatch(accountDatasets, range, { includeScheduledInvocations: true }))); + for (const dataset of zoneDatasets) { + results.push(await this.collectZoneBatch(dataset, range)); + } + + return results; + } + + private async collectAccountBatch( + datasets: readonly DatasetQuery[], + range: { start: Date; end: Date }, + options: { includeScheduledInvocations: boolean }, + ): Promise { + if (datasets.length === 0 && !options.includeScheduledInvocations) { + return []; + } + const results: CollectionResult[] = []; + const startedAt = performance.now(); + let batchResult: Awaited>; + try { + batchResult = await this.client.fetchAccountBatch(this.accountTag, datasets, range, options); + } catch (error) { + const durationMs = performance.now() - startedAt; + const message = errorMessage(error); + console.error('[collector] account batch fetch failed:', message); + for (const dataset of datasets) { + this.pushDatasetErrorMetric(dataset.key, error, durationMs, 'batch'); + results.push({ dataset: dataset.key, rows: 0, points: 0, durationMs, error: message }); + } + if (options.includeScheduledInvocations) { + this.pushDatasetErrorMetric('workers_scheduled', error, durationMs, 'batch'); + results.push({ dataset: 'workers_scheduled', rows: 0, points: 0, durationMs, error: message }); + } + return results; + } + + for (const dataset of datasets) { + const perStart = performance.now(); + if (batchResult.errors[dataset.key]) { + const msg = batchResult.errors[dataset.key]; + console.error(`[collector] ${dataset.key} field error:`, msg); + this.pushDatasetErrorMetric(dataset.key, new Error(msg), performance.now() - perStart); + results.push({ + dataset: dataset.key, + rows: 0, + points: 0, + durationMs: performance.now() - perStart, + error: msg, + }); + continue; + } + const rows = batchResult.rows[dataset.key] ?? []; + if (dataset.field === 'httpRequestsOverviewAdaptiveGroups') { + // Pages projects have zone tags in analytics but aren't in the + // bulk /zones list; resolve them individually for human-readable names. + await this.resolveZonesForRows(rows); + } + const points = this.emitRows(dataset, rows); + const durationMs = performance.now() - perStart; + this.metrics.push( + Metric.create('collector_dataset') + .addTag('dataset', dataset.key) + .addTag('status', 'success') + .intField('rows', rows.length) + .intField('points', points) + .durationField('duration', durationMs), + ); + results.push({ dataset: dataset.key, rows: rows.length, points, durationMs }); + } + + if (options.includeScheduledInvocations) { + results.push(this.emitScheduledInvocations(batchResult)); + } + return results; + } + + private emitScheduledInvocations( + batchResult: Awaited>, + ): CollectionResult { + const key = 'workers_scheduled'; + const startedAt = performance.now(); + if (batchResult.errors[key]) { + const msg = batchResult.errors[key]; + console.error(`[collector] ${key} field error:`, msg); + this.pushDatasetErrorMetric(key, new Error(msg), performance.now() - startedAt); + return { dataset: key, rows: 0, points: 0, durationMs: performance.now() - startedAt, error: msg }; + } + const events = batchResult.scheduledInvocations ?? []; + type Bucket = { + scriptName: string; + cron: string; + status: string; + minute: string; + count: number; + cpuTimeUs: number; + cpuTimeMax: number; + }; + const buckets = new Map(); + for (const event of events) { + if (!event.datetime) { + continue; + } + const minute = event.datetime.slice(0, 16) + ':00Z'; + const bucketKey = `${event.scriptName}|${event.cron}|${event.status}|${minute}`; + let bucket = buckets.get(bucketKey); + if (!bucket) { + bucket = { + scriptName: event.scriptName, + cron: event.cron, + status: event.status, + minute, + count: 0, + cpuTimeUs: 0, + cpuTimeMax: 0, + }; + buckets.set(bucketKey, bucket); + } + bucket.count++; + bucket.cpuTimeUs += event.cpuTimeUs ?? 0; + bucket.cpuTimeMax = Math.max(bucket.cpuTimeMax, event.cpuTimeUs ?? 0); + } + + let points = 0; + for (const bucket of buckets.values()) { + const timestamp = new Date(bucket.minute); + if (Number.isNaN(timestamp.getTime())) { + continue; + } + const metric = Metric.create('cf_workers_scheduled'); + metric.addTag('account_id', this.accountTag); + if (bucket.scriptName) { + metric.addTag('script_name', bucket.scriptName); + } + if (bucket.cron) { + metric.addTag('cron', bucket.cron); + } + if (bucket.status) { + metric.addTag('status', bucket.status); + } + metric + .intField('invocations', bucket.count) + .intField('cpu_time_us_sum', Math.round(bucket.cpuTimeUs)) + .intField('cpu_time_us_avg', bucket.count > 0 ? Math.round(bucket.cpuTimeUs / bucket.count) : 0) + .intField('cpu_time_us_max', Math.round(bucket.cpuTimeMax)) + .setExportTimestamp(timestamp); + this.metrics.pushRaw(metric); + points++; + } + + const durationMs = performance.now() - startedAt; + this.metrics.push( + Metric.create('collector_dataset') + .addTag('dataset', key) + .addTag('status', 'success') + .intField('rows', events.length) + .intField('points', points) + .durationField('duration', durationMs), + ); + return { dataset: key, rows: events.length, points, durationMs }; + } + + private async collectZoneBatch(dataset: DatasetQuery, range: { start: Date; end: Date }): Promise { + const startedAt = performance.now(); + // Only bulk-listed zones (real account zones), not Pages project zones. + // Iterate the bulk tag set directly and do a single Map lookup per tag — + // avoids spreading the full zones map and an extra filter pass. + const cache = this.resourceCache.getCache(); + const zoneTags: string[] = []; + const zoneNames: string[] = []; + for (const tag of cache.bulkZoneTags) { + const name = cache.zones.get(tag); + if (name !== undefined) { + zoneTags.push(tag); + zoneNames.push(name); + } + } + if (zoneTags.length === 0) { + this.metrics.push( + Metric.create('collector_dataset') + .addTag('dataset', dataset.key) + .addTag('status', 'skipped') + .addTag('reason', 'no_zones_in_cache') + .intField('rows', 0) + .intField('points', 0) + .durationField('duration', 0), + ); + return { dataset: dataset.key, rows: 0, points: 0, durationMs: 0 }; + } + let batchResult: Awaited>; + try { + batchResult = await this.client.fetchZoneBatch(zoneTags, dataset, range); + } catch (error) { + const durationMs = performance.now() - startedAt; + const message = errorMessage(error); + console.error(`[collector] ${dataset.key} zone batch fetch failed:`, message); + this.pushDatasetErrorMetric(dataset.key, error, durationMs, 'zone'); + return { dataset: dataset.key, rows: 0, points: 0, durationMs, error: message }; + } + + let totalRows = 0; + let totalPoints = 0; + let zoneErrors = 0; + for (let i = 0; i < zoneTags.length; i++) { + const zoneTag = zoneTags[i]; + const zoneName = zoneNames[i]; + if (batchResult.errors[zoneTag]) { + zoneErrors++; + console.error(`[collector] ${dataset.key} zone ${zoneTag} error:`, batchResult.errors[zoneTag]); + continue; + } + const rows = batchResult.rows[zoneTag] ?? []; + totalRows += rows.length; + for (const row of rows) { + // Mutate in-place — we own the row (just parsed from JSON) and + // avoiding the spread is a meaningful saving in the hot path. + if (row.dimensions) { + row.dimensions.zoneTag = zoneTag; + } else { + row.dimensions = { zoneTag }; + } + const metric = buildMetric(dataset, row, this.accountTag, cache); + if (metric) { + metric.addTag('zone_tag', zoneTag); + metric.addTag('zone_name', zoneName); + this.metrics.pushRaw(metric); + totalPoints++; + } + } + } + + const durationMs = performance.now() - startedAt; + const status = zoneErrors === 0 ? 'success' : zoneErrors === zoneTags.length ? 'error' : 'partial'; + this.metrics.push( + Metric.create('collector_dataset') + .addTag('dataset', dataset.key) + .addTag('status', status) + .intField('rows', totalRows) + .intField('points', totalPoints) + .intField('zones', zoneTags.length) + .intField('zone_errors', zoneErrors) + .durationField('duration', durationMs), + ); + return { + dataset: dataset.key, + rows: totalRows, + points: totalPoints, + durationMs, + error: zoneErrors === zoneTags.length ? 'all zones errored' : undefined, + }; + } + + private emitRows(dataset: DatasetQuery, rows: DatasetRow[]): number { + const cache = this.resourceCache.getCache(); + let emitted = 0; + for (const row of rows) { + const metric = buildMetric(dataset, row, this.accountTag, cache); + if (metric) { + this.metrics.pushRaw(metric); + emitted++; + } + } + return emitted; + } + + private async resolveZonesForRows(rows: DatasetRow[]): Promise { + const missing = new Set(); + for (const row of rows) { + const tag = normalizeTagValue(row.dimensions?.zoneTag); + if (tag) { + missing.add(tag); + } + } + await this.resourceCache.resolveMissingZones(missing); + } + + private pushDatasetErrorMetric( + datasetKey: string, + error: unknown, + durationMs: number, + scope: 'batch' | 'field' | 'zone' = 'field', + ): void { + this.metrics.push( + Metric.create('collector_dataset') + .addTag('dataset', datasetKey) + .addTag('status', 'error') + .addTag('error', errorTag(error)) + .addTag('error_scope', scope) + .addTag('error_message', truncateTag(errorMessage(error), 128)) + .intField('errors', 1) + .durationField('duration', durationMs), + ); + } +} + +function errorMessage(error: unknown): string { + if (error instanceof CloudflareGraphQLError) { + // Include the response body snippet when available — it often contains + // the real reason (rate limit, auth, etc). + const body = error.responseBody?.slice(0, 200) ?? ''; + return body ? `${error.message}: ${body}` : error.message; + } + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +function errorTag(error: unknown): string { + if (error instanceof CloudflareGraphQLError) { + return `graphql_${error.statusCode}`; + } + if (error instanceof Error) { + // Classify common Cloudflare GraphQL error messages into stable tags. + const msg = error.message.toLowerCase(); + if (msg.includes('rate limit') || msg.includes('throttl')) { + return 'rate_limited'; + } + if (msg.includes('timeout') || msg.includes('timed out')) { + return 'timeout'; + } + if (msg.includes('unauthorized') || msg.includes('forbidden') || msg.includes('authentication')) { + return 'auth_error'; + } + if (msg.includes('not found') || msg.includes('does not exist')) { + return 'not_found'; + } + if (msg.includes('internal server error') || msg.includes('internal error')) { + return 'internal_error'; + } + return error.name === 'Error' ? 'graphql_field_error' : error.name; + } + return 'unknown'; +} + +/** Truncate a string for use as a metric tag value. */ +function truncateTag(value: string, maxLength: number): string { + if (value.length <= maxLength) { + return value; + } + return value.slice(0, maxLength - 3) + '...'; +} diff --git a/apps/cloudflare-metrics/src/datasets.test.ts b/apps/cloudflare-metrics/src/datasets.test.ts new file mode 100644 index 0000000..7af6e44 --- /dev/null +++ b/apps/cloudflare-metrics/src/datasets.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { ALL_DATASETS } from './datasets.js'; + +describe('dataset registry invariants', () => { + it('every dataset defines at least one tag or field', () => { + for (const dataset of ALL_DATASETS) { + const fieldCount = Object.keys(dataset.fields).length; + expect(fieldCount, `${dataset.key} must have fields`).toBeGreaterThan(0); + for (const [, spec] of Object.entries(dataset.fields)) { + expect(['int', 'float']).toContain(spec.type); + expect(spec.source[0]).toMatch(/^(sum|avg|max|min|quantiles|uniq|_top)$/); + } + } + }); + + it('every dataset selects its timestamp dimension', () => { + for (const dataset of ALL_DATASETS) { + const timestampDim = dataset.timestampDimension ?? 'datetimeMinute'; + expect(dataset.dimensions, `${dataset.key} must include ${timestampDim}`).toContain(timestampDim); + } + }); + + it('measurement names are unique and cf_-prefixed', () => { + const seen = new Set(); + for (const dataset of ALL_DATASETS) { + expect(dataset.measurement).toMatch(/^cf_/); + expect(seen.has(dataset.measurement)).toBe(false); + seen.add(dataset.measurement); + } + }); +}); diff --git a/apps/cloudflare-metrics/src/datasets.ts b/apps/cloudflare-metrics/src/datasets.ts new file mode 100644 index 0000000..016e5d5 --- /dev/null +++ b/apps/cloudflare-metrics/src/datasets.ts @@ -0,0 +1,1871 @@ +import type { DatasetQuery } from './types.js'; + +/** + * One-minute bucket granularity via the `datetimeMinute` dimension — the + * finest grouping the Cloudflare Analytics API exposes. The exporter runs + * on a 5-minute cron and the collector uses a wider window so each run + * overlaps with the previous one (VictoriaMetrics dedupes by series + + * timestamp so overlap is free). + * + * Dimension → tag mapping and field names use snake_case to match the + * existing VictoriaMetrics conventions in other workers. + */ + +export const WORKERS_INVOCATIONS: DatasetQuery = { + key: 'workers_invocations', + measurement: 'cf_workers_invocations', + field: 'workersInvocationsAdaptive', + dimensions: ['datetimeMinute', 'scriptName', 'status', 'scriptVersion', 'usageModel'], + blocks: { + sum: [ + 'requests', + 'errors', + 'subrequests', + 'clientDisconnects', + 'cpuTimeUs', + 'duration', + 'wallTime', + 'responseBodySize', + ], + max: ['cpuTime', 'duration', 'wallTime', 'requestDuration', 'responseBodySize'], + quantiles: [ + 'cpuTimeP50', + 'cpuTimeP99', + 'durationP50', + 'durationP99', + 'wallTimeP50', + 'wallTimeP99', + 'responseBodySizeP99', + ], + }, + tags: [ + { source: 'scriptName', as: 'script_name' }, + { source: 'status', as: 'status' }, + { source: 'scriptVersion', as: 'script_version' }, + { source: 'usageModel', as: 'usage_model' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + errors: { type: 'int', source: ['sum', 'errors'] }, + subrequests: { type: 'int', source: ['sum', 'subrequests'] }, + client_disconnects: { type: 'int', source: ['sum', 'clientDisconnects'] }, + cpu_time_us_sum: { type: 'int', source: ['sum', 'cpuTimeUs'] }, + // `duration` here is seconds from the API — convert to milliseconds. + duration_ms_sum: { type: 'float', source: ['sum', 'duration'], scale: 1000 }, + wall_time_us_sum: { type: 'int', source: ['sum', 'wallTime'] }, + response_body_size_bytes_sum: { type: 'int', source: ['sum', 'responseBodySize'] }, + cpu_time_us_max: { type: 'int', source: ['max', 'cpuTime'] }, + duration_ms_max: { type: 'float', source: ['max', 'duration'], scale: 1000 }, + wall_time_us_max: { type: 'int', source: ['max', 'wallTime'] }, + request_duration_us_max: { type: 'int', source: ['max', 'requestDuration'] }, + response_body_size_bytes_max: { type: 'int', source: ['max', 'responseBodySize'] }, + cpu_time_us_p50: { type: 'int', source: ['quantiles', 'cpuTimeP50'] }, + cpu_time_us_p99: { type: 'int', source: ['quantiles', 'cpuTimeP99'] }, + duration_ms_p50: { type: 'float', source: ['quantiles', 'durationP50'], scale: 1000 }, + duration_ms_p99: { type: 'float', source: ['quantiles', 'durationP99'], scale: 1000 }, + wall_time_us_p50: { type: 'int', source: ['quantiles', 'wallTimeP50'] }, + wall_time_us_p99: { type: 'int', source: ['quantiles', 'wallTimeP99'] }, + response_body_size_bytes_p99: { type: 'int', source: ['quantiles', 'responseBodySizeP99'] }, + }, +}; + +export const WORKERS_SUBREQUESTS: DatasetQuery = { + key: 'workers_subrequests', + measurement: 'cf_workers_subrequests', + field: 'workersSubrequestsAdaptiveGroups', + dimensions: ['datetimeMinute', 'scriptName', 'hostname', 'cacheStatus', 'httpResponseStatus', 'requestOutcome'], + blocks: { + sum: [ + 'subrequests', + 'requestBodySize', + 'requestBodySizeUncached', + 'responseBodySize', + 'timeToResponseUs', + 'timeToResponseDrainedUs', + ], + }, + tags: [ + { source: 'scriptName', as: 'script_name' }, + { source: 'hostname', as: 'hostname' }, + { source: 'cacheStatus', as: 'cache_status' }, + { source: 'httpResponseStatus', as: 'http_response_status' }, + { source: 'requestOutcome', as: 'request_outcome' }, + ], + fields: { + subrequests: { type: 'int', source: ['sum', 'subrequests'] }, + request_body_size_bytes: { type: 'int', source: ['sum', 'requestBodySize'] }, + request_body_size_uncached_bytes: { type: 'int', source: ['sum', 'requestBodySizeUncached'] }, + response_body_size_bytes: { type: 'int', source: ['sum', 'responseBodySize'] }, + time_to_response_us: { type: 'int', source: ['sum', 'timeToResponseUs'] }, + time_to_response_drained_us: { type: 'int', source: ['sum', 'timeToResponseDrainedUs'] }, + }, +}; + +export const WORKERS_OVERVIEW: DatasetQuery = { + key: 'workers_overview', + measurement: 'cf_workers_overview', + field: 'workersOverviewRequestsAdaptiveGroups', + dimensions: ['datetimeMinute', 'status', 'usageModel'], + blocks: { sum: ['cpuTimeUs'] }, + tags: [ + { source: 'status', as: 'status' }, + { source: 'usageModel', as: 'usage_model' }, + ], + fields: { + cpu_time_us: { type: 'int', source: ['sum', 'cpuTimeUs'] }, + }, +}; + +export const D1_QUERIES: DatasetQuery = { + key: 'd1_queries', + measurement: 'cf_d1_queries', + field: 'd1AnalyticsAdaptiveGroups', + dimensions: ['datetimeMinute', 'databaseId', 'databaseRole'], + blocks: { + sum: ['readQueries', 'writeQueries', 'rowsRead', 'rowsWritten', 'queryBatchResponseBytes'], + avg: ['queryBatchTimeMs', 'queryBatchResponseBytes', 'sampleInterval'], + }, + tags: [ + { source: 'databaseId', as: 'database_id' }, + { source: 'databaseRole', as: 'database_role' }, + ], + fields: { + read_queries: { type: 'int', source: ['sum', 'readQueries'] }, + write_queries: { type: 'int', source: ['sum', 'writeQueries'] }, + rows_read: { type: 'int', source: ['sum', 'rowsRead'] }, + rows_written: { type: 'int', source: ['sum', 'rowsWritten'] }, + response_bytes_sum: { type: 'int', source: ['sum', 'queryBatchResponseBytes'] }, + query_duration_ms_avg: { type: 'float', source: ['avg', 'queryBatchTimeMs'] }, + response_bytes_avg: { type: 'float', source: ['avg', 'queryBatchResponseBytes'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const D1_STORAGE: DatasetQuery = { + key: 'd1_storage', + measurement: 'cf_d1_storage', + field: 'd1StorageAdaptiveGroups', + dimensions: ['datetimeMinute', 'databaseId'], + blocks: { max: ['databaseSizeBytes'] }, + tags: [{ source: 'databaseId', as: 'database_id' }], + fields: { + database_size_bytes: { type: 'int', source: ['max', 'databaseSizeBytes'] }, + }, +}; + +export const R2_OPERATIONS: DatasetQuery = { + key: 'r2_operations', + measurement: 'cf_r2_operations', + field: 'r2OperationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'bucketName', 'actionType', 'actionStatus', 'responseStatusCode', 'storageClass'], + blocks: { + sum: ['requests', 'responseBytes', 'responseObjectSize'], + }, + tags: [ + { source: 'bucketName', as: 'bucket_name' }, + { source: 'actionType', as: 'action_type' }, + { source: 'actionStatus', as: 'action_status' }, + { source: 'responseStatusCode', as: 'response_status_code' }, + { source: 'storageClass', as: 'storage_class' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + response_bytes: { type: 'int', source: ['sum', 'responseBytes'] }, + response_object_bytes: { type: 'int', source: ['sum', 'responseObjectSize'] }, + }, +}; + +export const R2_STORAGE: DatasetQuery = { + key: 'r2_storage', + measurement: 'cf_r2_storage', + field: 'r2StorageAdaptiveGroups', + dimensions: ['datetimeMinute', 'bucketName', 'storageClass'], + blocks: { max: ['objectCount', 'payloadSize', 'metadataSize', 'uploadCount'] }, + tags: [ + { source: 'bucketName', as: 'bucket_name' }, + { source: 'storageClass', as: 'storage_class' }, + ], + fields: { + object_count: { type: 'int', source: ['max', 'objectCount'] }, + payload_bytes: { type: 'int', source: ['max', 'payloadSize'] }, + metadata_bytes: { type: 'int', source: ['max', 'metadataSize'] }, + upload_count: { type: 'int', source: ['max', 'uploadCount'] }, + }, +}; + +export const KV_OPERATIONS: DatasetQuery = { + key: 'kv_operations', + measurement: 'cf_kv_operations', + field: 'kvOperationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'namespaceId', 'actionType', 'result', 'responseStatusCode'], + blocks: { sum: ['requests', 'objectBytes'] }, + tags: [ + { source: 'namespaceId', as: 'namespace_id' }, + { source: 'actionType', as: 'action_type' }, + { source: 'result', as: 'result' }, + { source: 'responseStatusCode', as: 'response_status_code' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + object_bytes: { type: 'int', source: ['sum', 'objectBytes'] }, + }, +}; + +export const KV_STORAGE: DatasetQuery = { + key: 'kv_storage', + measurement: 'cf_kv_storage', + field: 'kvStorageAdaptiveGroups', + dimensions: ['datetimeMinute', 'namespaceId'], + blocks: { max: ['byteCount', 'keyCount'] }, + tags: [{ source: 'namespaceId', as: 'namespace_id' }], + fields: { + byte_count: { type: 'int', source: ['max', 'byteCount'] }, + key_count: { type: 'int', source: ['max', 'keyCount'] }, + }, +}; + +export const DURABLE_OBJECTS_INVOCATIONS: DatasetQuery = { + key: 'durable_objects_invocations', + measurement: 'cf_durable_objects_invocations', + field: 'durableObjectsInvocationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'namespaceId', 'scriptName', 'status', 'type'], + blocks: { + sum: ['requests', 'errors', 'responseBodySize', 'wallTime'], + max: ['wallTime', 'responseBodySize'], + }, + tags: [ + { source: 'namespaceId', as: 'namespace_id' }, + { source: 'scriptName', as: 'script_name' }, + { source: 'status', as: 'status' }, + { source: 'type', as: 'type' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + errors: { type: 'int', source: ['sum', 'errors'] }, + response_body_size_bytes_sum: { type: 'int', source: ['sum', 'responseBodySize'] }, + wall_time_us_sum: { type: 'int', source: ['sum', 'wallTime'] }, + wall_time_us_max: { type: 'int', source: ['max', 'wallTime'] }, + response_body_size_bytes_max: { type: 'int', source: ['max', 'responseBodySize'] }, + }, +}; + +export const DURABLE_OBJECTS_PERIODIC: DatasetQuery = { + key: 'durable_objects_periodic', + measurement: 'cf_durable_objects_periodic', + field: 'durableObjectsPeriodicGroups', + dimensions: ['datetimeMinute', 'namespaceId'], + blocks: { + sum: [ + 'activeTime', + 'cpuTime', + 'duration', + 'exceededCpuErrors', + 'exceededMemoryErrors', + 'fatalInternalErrors', + 'inboundWebsocketMsgCount', + 'outboundWebsocketMsgCount', + 'rowsRead', + 'rowsWritten', + 'storageDeletes', + 'storageReadUnits', + 'storageWriteUnits', + 'subrequests', + ], + max: ['activeWebsocketConnections'], + }, + tags: [{ source: 'namespaceId', as: 'namespace_id' }], + fields: { + active_time_us: { type: 'int', source: ['sum', 'activeTime'] }, + cpu_time_us: { type: 'int', source: ['sum', 'cpuTime'] }, + duration_ms: { type: 'float', source: ['sum', 'duration'], scale: 1000 }, + exceeded_cpu_errors: { type: 'int', source: ['sum', 'exceededCpuErrors'] }, + exceeded_memory_errors: { type: 'int', source: ['sum', 'exceededMemoryErrors'] }, + fatal_internal_errors: { type: 'int', source: ['sum', 'fatalInternalErrors'] }, + inbound_ws_messages: { type: 'int', source: ['sum', 'inboundWebsocketMsgCount'] }, + outbound_ws_messages: { type: 'int', source: ['sum', 'outboundWebsocketMsgCount'] }, + rows_read: { type: 'int', source: ['sum', 'rowsRead'] }, + rows_written: { type: 'int', source: ['sum', 'rowsWritten'] }, + storage_deletes: { type: 'int', source: ['sum', 'storageDeletes'] }, + storage_read_units: { type: 'int', source: ['sum', 'storageReadUnits'] }, + storage_write_units: { type: 'int', source: ['sum', 'storageWriteUnits'] }, + subrequests: { type: 'int', source: ['sum', 'subrequests'] }, + active_ws_connections_max: { type: 'int', source: ['max', 'activeWebsocketConnections'] }, + }, +}; + +export const DURABLE_OBJECTS_STORAGE: DatasetQuery = { + key: 'durable_objects_storage', + measurement: 'cf_durable_objects_storage', + field: 'durableObjectsStorageGroups', + dimensions: ['datetimeMinute'], + blocks: { max: ['storedBytes'] }, + tags: [], + fields: { + stored_bytes: { type: 'int', source: ['max', 'storedBytes'] }, + }, +}; + +export const DURABLE_OBJECTS_SQL_STORAGE: DatasetQuery = { + key: 'durable_objects_sql_storage', + measurement: 'cf_durable_objects_sql_storage', + field: 'durableObjectsSqlStorageGroups', + dimensions: ['datetimeMinute', 'namespaceId'], + blocks: { max: ['storedBytes'] }, + tags: [{ source: 'namespaceId', as: 'namespace_id' }], + fields: { + stored_bytes: { type: 'int', source: ['max', 'storedBytes'] }, + }, +}; + +export const DURABLE_OBJECTS_SUBREQUESTS: DatasetQuery = { + key: 'durable_objects_subrequests', + measurement: 'cf_durable_objects_subrequests', + field: 'durableObjectsSubrequestsAdaptiveGroups', + dimensions: ['datetimeMinute', 'namespaceId', 'scriptName'], + blocks: { sum: ['requestBodySizeUncached'] }, + tags: [ + { source: 'namespaceId', as: 'namespace_id' }, + { source: 'scriptName', as: 'script_name' }, + ], + fields: { + request_body_size_uncached_bytes: { type: 'int', source: ['sum', 'requestBodySizeUncached'] }, + }, +}; + +export const QUEUE_OPERATIONS: DatasetQuery = { + key: 'queue_operations', + measurement: 'cf_queue_operations', + field: 'queueMessageOperationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'queueId', 'actionType', 'consumerType', 'outcome'], + blocks: { sum: ['billableOperations', 'bytes'] }, + tags: [ + { source: 'queueId', as: 'queue_id' }, + { source: 'actionType', as: 'action_type' }, + { source: 'consumerType', as: 'consumer_type' }, + { source: 'outcome', as: 'outcome' }, + ], + fields: { + billable_operations: { type: 'int', source: ['sum', 'billableOperations'] }, + bytes: { type: 'int', source: ['sum', 'bytes'] }, + }, +}; + +export const QUEUE_BACKLOG: DatasetQuery = { + key: 'queue_backlog', + measurement: 'cf_queue_backlog', + field: 'queueBacklogAdaptiveGroups', + dimensions: ['datetimeMinute', 'queueId'], + blocks: { avg: ['bytes', 'messages', 'sampleInterval'] }, + tags: [{ source: 'queueId', as: 'queue_id' }], + fields: { + backlog_bytes_avg: { type: 'float', source: ['avg', 'bytes'] }, + backlog_messages_avg: { type: 'float', source: ['avg', 'messages'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const HYPERDRIVE_QUERIES: DatasetQuery = { + key: 'hyperdrive_queries', + measurement: 'cf_hyperdrive_queries', + field: 'hyperdriveQueriesAdaptiveGroups', + dimensions: ['datetimeMinute', 'configId', 'cacheStatus', 'eventStatus', 'isFree'], + topLevelFields: ['count'], + blocks: { + sum: [ + 'queryBytes', + 'resultBytes', + 'queryLatency', + 'connectionLatency', + 'originReadLatency', + 'originWriteLatency', + 'clientWriteLatency', + ], + }, + tags: [ + { source: 'configId', as: 'config_id' }, + { source: 'cacheStatus', as: 'cache_status' }, + { source: 'eventStatus', as: 'event_status' }, + { source: 'isFree', as: 'is_free' }, + ], + fields: { + count: { type: 'int', source: ['_top', 'count'] }, + query_bytes: { type: 'int', source: ['sum', 'queryBytes'] }, + result_bytes: { type: 'int', source: ['sum', 'resultBytes'] }, + query_latency_us: { type: 'int', source: ['sum', 'queryLatency'] }, + connection_latency_us: { type: 'int', source: ['sum', 'connectionLatency'] }, + origin_read_latency_us: { type: 'int', source: ['sum', 'originReadLatency'] }, + origin_write_latency_us: { type: 'int', source: ['sum', 'originWriteLatency'] }, + client_write_latency_us: { type: 'int', source: ['sum', 'clientWriteLatency'] }, + }, +}; + +export const HYPERDRIVE_POOL: DatasetQuery = { + key: 'hyperdrive_pool', + measurement: 'cf_hyperdrive_pool', + field: 'hyperdrivePoolSizesAdaptiveGroups', + dimensions: ['datetimeMinute', 'configId', 'databaseType'], + blocks: { max: ['currentPoolSize', 'maxPoolSize', 'waitingClients'] }, + tags: [ + { source: 'configId', as: 'config_id' }, + { source: 'databaseType', as: 'database_type' }, + ], + fields: { + current_pool_size: { type: 'int', source: ['max', 'currentPoolSize'] }, + max_pool_size: { type: 'int', source: ['max', 'maxPoolSize'] }, + waiting_clients: { type: 'int', source: ['max', 'waitingClients'] }, + }, +}; + +export const HTTP_REQUESTS_OVERVIEW: DatasetQuery = { + key: 'http_requests_overview', + measurement: 'cf_http_requests_overview', + field: 'httpRequestsOverviewAdaptiveGroups', + dimensions: ['datetimeMinute', 'zoneTag', 'clientCountryName', 'clientRequestHTTPProtocol', 'edgeResponseStatus'], + blocks: { sum: ['requests', 'bytes', 'cachedRequests', 'cachedBytes', 'pageViews', 'visits'] }, + tags: [ + { source: 'zoneTag', as: 'zone_tag' }, + { source: 'clientCountryName', as: 'client_country' }, + { source: 'clientRequestHTTPProtocol', as: 'http_protocol' }, + { source: 'edgeResponseStatus', as: 'edge_response_status' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + bytes: { type: 'int', source: ['sum', 'bytes'] }, + cached_requests: { type: 'int', source: ['sum', 'cachedRequests'] }, + cached_bytes: { type: 'int', source: ['sum', 'cachedBytes'] }, + page_views: { type: 'int', source: ['sum', 'pageViews'] }, + visits: { type: 'int', source: ['sum', 'visits'] }, + }, +}; + +export const PAGES_FUNCTIONS_INVOCATIONS: DatasetQuery = { + key: 'pages_functions_invocations', + measurement: 'cf_pages_functions_invocations', + field: 'pagesFunctionsInvocationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'scriptName', 'status', 'usageModel'], + blocks: { + sum: ['requests', 'errors', 'clientDisconnects', 'duration', 'wallTime', 'subrequests', 'responseBodySize'], + quantiles: ['cpuTimeP50', 'cpuTimeP99', 'durationP50', 'durationP99'], + }, + tags: [ + { source: 'scriptName', as: 'script_name' }, + { source: 'status', as: 'status' }, + { source: 'usageModel', as: 'usage_model' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + errors: { type: 'int', source: ['sum', 'errors'] }, + client_disconnects: { type: 'int', source: ['sum', 'clientDisconnects'] }, + duration_ms_sum: { type: 'float', source: ['sum', 'duration'], scale: 1000 }, + wall_time_us_sum: { type: 'int', source: ['sum', 'wallTime'] }, + subrequests: { type: 'int', source: ['sum', 'subrequests'] }, + response_body_size_bytes_sum: { type: 'int', source: ['sum', 'responseBodySize'] }, + cpu_time_us_p50: { type: 'int', source: ['quantiles', 'cpuTimeP50'] }, + cpu_time_us_p99: { type: 'int', source: ['quantiles', 'cpuTimeP99'] }, + duration_ms_p50: { type: 'float', source: ['quantiles', 'durationP50'], scale: 1000 }, + duration_ms_p99: { type: 'float', source: ['quantiles', 'durationP99'], scale: 1000 }, + }, +}; + +export const D1_QUERIES_DETAIL: DatasetQuery = { + key: 'd1_queries_detail', + measurement: 'cf_d1_queries_detail', + field: 'd1QueriesAdaptiveGroups', + dimensions: ['datetimeMinute', 'databaseId', 'databaseRole', 'error'], + topLevelFields: ['count'], + blocks: { + sum: ['queryDurationMs', 'rowsRead', 'rowsReturned', 'rowsWritten'], + avg: ['queryDurationMs', 'sampleInterval'], + quantiles: ['queryDurationMsP50', 'queryDurationMsP95', 'queryDurationMsP99'], + }, + tags: [ + { source: 'databaseId', as: 'database_id' }, + { source: 'databaseRole', as: 'database_role' }, + { source: 'error', as: 'error' }, + ], + fields: { + query_count: { type: 'int', source: ['_top', 'count'] }, + query_duration_ms_sum: { type: 'float', source: ['sum', 'queryDurationMs'] }, + rows_read: { type: 'int', source: ['sum', 'rowsRead'] }, + rows_returned: { type: 'int', source: ['sum', 'rowsReturned'] }, + rows_written: { type: 'int', source: ['sum', 'rowsWritten'] }, + query_duration_ms_avg: { type: 'float', source: ['avg', 'queryDurationMs'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + query_duration_ms_p50: { type: 'float', source: ['quantiles', 'queryDurationMsP50'] }, + query_duration_ms_p95: { type: 'float', source: ['quantiles', 'queryDurationMsP95'] }, + query_duration_ms_p99: { type: 'float', source: ['quantiles', 'queryDurationMsP99'] }, + }, +}; + +export const QUEUE_CONSUMER: DatasetQuery = { + key: 'queue_consumer', + measurement: 'cf_queue_consumer', + field: 'queueConsumerMetricsAdaptiveGroups', + dimensions: ['datetimeMinute', 'queueId'], + blocks: { avg: ['concurrency', 'sampleInterval'] }, + tags: [{ source: 'queueId', as: 'queue_id' }], + fields: { + concurrency_avg: { type: 'float', source: ['avg', 'concurrency'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +// Note: `firewallEventsAdaptiveGroups` is gated on Business/Enterprise plans and +// returns `authz`/"does not have access to the path" on free/Pro accounts. We +// leave it out of the registry rather than failing loudly every cron tick — +// if the plan level changes, adding an entry here with +// `scope: 'zone', field: 'firewallEventsAdaptiveGroups'` will be enough. + +export const HTTP_REQUESTS_DETAIL: DatasetQuery = { + key: 'http_requests_detail', + measurement: 'cf_http_requests_detail', + field: 'httpRequestsAdaptiveGroups', + // `httpRequestsAdaptiveGroups` is gated at the account level on non-Enterprise + // plans but works at the zone level. The collector iterates over the cached + // zones (bulk list + Pages lookups) and runs one query per zone. + scope: 'zone', + dimensions: [ + 'datetimeMinute', + 'clientCountryName', + 'clientRequestHTTPMethodName', + 'clientRequestHTTPProtocol', + 'edgeResponseStatus', + 'cacheStatus', + ], + topLevelFields: ['count'], + blocks: { + sum: ['edgeRequestBytes', 'edgeResponseBytes', 'visits'], + avg: ['sampleInterval'], + }, + tags: [ + { source: 'clientCountryName', as: 'client_country' }, + { source: 'clientRequestHTTPMethodName', as: 'http_method' }, + { source: 'clientRequestHTTPProtocol', as: 'http_protocol' }, + { source: 'edgeResponseStatus', as: 'edge_response_status' }, + { source: 'cacheStatus', as: 'cache_status' }, + ], + fields: { + requests: { type: 'int', source: ['_top', 'count'] }, + edge_request_bytes: { type: 'int', source: ['sum', 'edgeRequestBytes'] }, + edge_response_bytes: { type: 'int', source: ['sum', 'edgeResponseBytes'] }, + visits: { type: 'int', source: ['sum', 'visits'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +// ============================================================================= +// Additional account-level datasets. These cover products the account may or +// may not currently be using — dimensions are picked conservatively to keep +// cardinality manageable, and datasets with no current data just return zero +// rows and no-op. +// ============================================================================= + +export const WORKERS_ANALYTICS_ENGINE: DatasetQuery = { + key: 'workers_analytics_engine', + measurement: 'cf_workers_analytics_engine', + field: 'workersAnalyticsEngineAdaptiveGroups', + dimensions: ['datetimeMinute', 'dataset'], + topLevelFields: ['count'], + blocks: {}, + tags: [{ source: 'dataset', as: 'dataset' }], + fields: { events: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const WORKERS_BUILDS: DatasetQuery = { + key: 'workers_builds', + measurement: 'cf_workers_builds', + field: 'workersBuildsBuildMinutesAdaptiveGroups', + dimensions: ['datetimeMinute'], + blocks: { sum: ['buildMinutes'] }, + tags: [], + fields: { build_minutes: { type: 'float', source: ['sum', 'buildMinutes'] } }, +}; + +export const WORKERS_VPC: DatasetQuery = { + key: 'workers_vpc', + measurement: 'cf_workers_vpc', + field: 'workersVpcConnectionAdaptiveGroups', + dimensions: ['datetimeMinute', 'targetId', 'status', 'errorCode'], + topLevelFields: ['count'], + blocks: { + sum: ['connectionLatency', 'dnsLatency'], + avg: ['connectionLatency', 'dnsLatency'], + }, + tags: [ + { source: 'targetId', as: 'target_id' }, + { source: 'status', as: 'status' }, + { source: 'errorCode', as: 'error_code' }, + ], + fields: { + connections: { type: 'int', source: ['_top', 'count'] }, + connection_latency_ms_sum: { type: 'float', source: ['sum', 'connectionLatency'] }, + dns_latency_ms_sum: { type: 'float', source: ['sum', 'dnsLatency'] }, + connection_latency_ms_avg: { type: 'float', source: ['avg', 'connectionLatency'] }, + dns_latency_ms_avg: { type: 'float', source: ['avg', 'dnsLatency'] }, + }, +}; + +export const WORKER_PLACEMENT: DatasetQuery = { + key: 'worker_placement', + measurement: 'cf_worker_placement', + field: 'workerPlacementAdaptiveGroups', + dimensions: ['datetimeMinute', 'scriptName', 'placementUsed', 'httpStatus'], + blocks: { + sum: ['requests', 'requestDuration'], + quantiles: ['requestDurationP50', 'requestDurationP95', 'requestDurationP99'], + }, + tags: [ + { source: 'scriptName', as: 'script_name' }, + { source: 'placementUsed', as: 'placement_used' }, + { source: 'httpStatus', as: 'http_status' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + request_duration_ms_sum: { type: 'float', source: ['sum', 'requestDuration'] }, + request_duration_ms_p50: { type: 'float', source: ['quantiles', 'requestDurationP50'] }, + request_duration_ms_p95: { type: 'float', source: ['quantiles', 'requestDurationP95'] }, + request_duration_ms_p99: { type: 'float', source: ['quantiles', 'requestDurationP99'] }, + }, +}; + +export const WORKFLOWS: DatasetQuery = { + key: 'workflows', + measurement: 'cf_workflows', + field: 'workflowsAdaptiveGroups', + dimensions: ['datetimeMinute', 'workflowName', 'eventType'], + topLevelFields: ['count'], + blocks: { + sum: ['allStepCount', 'cpuTime', 'executionDuration', 'retryCount', 'stepCount', 'storageRate', 'wallTime'], + avg: ['cpuTime', 'wallTime'], + }, + tags: [ + { source: 'workflowName', as: 'workflow_name' }, + { source: 'eventType', as: 'event_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + all_steps: { type: 'int', source: ['sum', 'allStepCount'] }, + cpu_time_us_sum: { type: 'int', source: ['sum', 'cpuTime'] }, + execution_duration_ms_sum: { type: 'float', source: ['sum', 'executionDuration'] }, + retries: { type: 'int', source: ['sum', 'retryCount'] }, + steps: { type: 'int', source: ['sum', 'stepCount'] }, + storage_rate: { type: 'float', source: ['sum', 'storageRate'] }, + wall_time_us_sum: { type: 'int', source: ['sum', 'wallTime'] }, + cpu_time_us_avg: { type: 'int', source: ['avg', 'cpuTime'] }, + wall_time_us_avg: { type: 'int', source: ['avg', 'wallTime'] }, + }, +}; + +export const AI_GATEWAY_REQUESTS: DatasetQuery = { + key: 'ai_gateway_requests', + measurement: 'cf_ai_gateway_requests', + field: 'aiGatewayRequestsAdaptiveGroups', + dimensions: ['datetimeMinute', 'gateway', 'provider', 'model', 'cached', 'error', 'rateLimited'], + topLevelFields: ['count'], + blocks: { + sum: [ + 'cachedRequests', + 'cachedTokensIn', + 'cachedTokensOut', + 'cost', + 'erroredRequests', + 'uncachedTokensIn', + 'uncachedTokensOut', + ], + }, + tags: [ + { source: 'gateway', as: 'gateway' }, + { source: 'provider', as: 'provider' }, + { source: 'model', as: 'model' }, + { source: 'cached', as: 'cached' }, + { source: 'error', as: 'error' }, + { source: 'rateLimited', as: 'rate_limited' }, + ], + fields: { + requests: { type: 'int', source: ['_top', 'count'] }, + cached_requests: { type: 'int', source: ['sum', 'cachedRequests'] }, + cached_tokens_in: { type: 'int', source: ['sum', 'cachedTokensIn'] }, + cached_tokens_out: { type: 'int', source: ['sum', 'cachedTokensOut'] }, + cost: { type: 'float', source: ['sum', 'cost'] }, + errored_requests: { type: 'int', source: ['sum', 'erroredRequests'] }, + uncached_tokens_in: { type: 'int', source: ['sum', 'uncachedTokensIn'] }, + uncached_tokens_out: { type: 'int', source: ['sum', 'uncachedTokensOut'] }, + }, +}; + +export const AI_GATEWAY_CACHE: DatasetQuery = { + key: 'ai_gateway_cache', + measurement: 'cf_ai_gateway_cache', + field: 'aiGatewayCacheAdaptiveGroups', + dimensions: ['datetimeMinute', 'gateway', 'provider', 'model', 'cacheOp'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'gateway', as: 'gateway' }, + { source: 'provider', as: 'provider' }, + { source: 'model', as: 'model' }, + { source: 'cacheOp', as: 'cache_op' }, + ], + fields: { events: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const AI_GATEWAY_ERRORS: DatasetQuery = { + key: 'ai_gateway_errors', + measurement: 'cf_ai_gateway_errors', + field: 'aiGatewayErrorsAdaptiveGroups', + dimensions: ['datetimeMinute', 'gateway', 'provider', 'model'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'gateway', as: 'gateway' }, + { source: 'provider', as: 'provider' }, + { source: 'model', as: 'model' }, + ], + fields: { errors: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const AI_GATEWAY_SIZE: DatasetQuery = { + key: 'ai_gateway_size', + measurement: 'cf_ai_gateway_size', + field: 'aiGatewaySizeAdaptiveGroups', + dimensions: ['datetimeMinute', 'gateway', 'logManagement'], + blocks: { max: ['rows'] }, + tags: [ + { source: 'gateway', as: 'gateway' }, + { source: 'logManagement', as: 'log_management' }, + ], + fields: { rows_max: { type: 'int', source: ['max', 'rows'] } }, +}; + +export const AI_INFERENCE: DatasetQuery = { + key: 'ai_inference', + measurement: 'cf_ai_inference', + field: 'aiInferenceAdaptiveGroups', + dimensions: ['datetimeMinute', 'modelId', 'errorCode', 'requestSource', 'tag'], + topLevelFields: ['count'], + blocks: { + sum: [ + 'totalAudioSeconds', + 'totalInferenceSteps', + 'totalInferenceTimeMs', + 'totalInputLength', + 'totalInputTokens', + 'totalNeurons', + 'totalOutputTokens', + 'totalProcessedPixels', + 'totalProcessedTiles', + 'totalRequestBytesIn', + 'totalRequestBytesOut', + 'totalTiles', + ], + }, + tags: [ + { source: 'modelId', as: 'model_id' }, + { source: 'errorCode', as: 'error_code' }, + { source: 'requestSource', as: 'request_source' }, + { source: 'tag', as: 'tag' }, + ], + fields: { + requests: { type: 'int', source: ['_top', 'count'] }, + audio_seconds: { type: 'float', source: ['sum', 'totalAudioSeconds'] }, + inference_steps: { type: 'int', source: ['sum', 'totalInferenceSteps'] }, + inference_time_ms: { type: 'float', source: ['sum', 'totalInferenceTimeMs'] }, + input_length: { type: 'int', source: ['sum', 'totalInputLength'] }, + input_tokens: { type: 'int', source: ['sum', 'totalInputTokens'] }, + neurons: { type: 'int', source: ['sum', 'totalNeurons'] }, + output_tokens: { type: 'int', source: ['sum', 'totalOutputTokens'] }, + processed_pixels: { type: 'int', source: ['sum', 'totalProcessedPixels'] }, + processed_tiles: { type: 'int', source: ['sum', 'totalProcessedTiles'] }, + request_bytes_in: { type: 'int', source: ['sum', 'totalRequestBytesIn'] }, + request_bytes_out: { type: 'int', source: ['sum', 'totalRequestBytesOut'] }, + tiles: { type: 'int', source: ['sum', 'totalTiles'] }, + }, +}; + +export const AI_SEARCH_API: DatasetQuery = { + key: 'ai_search_api', + measurement: 'cf_ai_search_api', + field: 'aiSearchAPIAdaptiveGroups', + dimensions: ['datetimeMinute', 'searchType', 'aiSearchModel'], + topLevelFields: ['count'], + blocks: { sum: ['aiSearchCount', 'searchCount'] }, + tags: [ + { source: 'searchType', as: 'search_type' }, + { source: 'aiSearchModel', as: 'ai_search_model' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + ai_search_count: { type: 'int', source: ['sum', 'aiSearchCount'] }, + search_count: { type: 'int', source: ['sum', 'searchCount'] }, + }, +}; + +export const AI_SEARCH_INGESTED_ITEMS: DatasetQuery = { + key: 'ai_search_ingested', + measurement: 'cf_ai_search_ingested', + field: 'aiSearchIngestedItemsAdaptiveGroups', + dimensions: ['datetimeMinute', 'indexId', 'embeddingModel', 'error'], + topLevelFields: ['count'], + blocks: { + sum: [ + 'chunkingDurationMs', + 'embeddingDurationMs', + 'fileSizeBytes', + 'numChunks', + 'totalDurationMs', + 'totalTokens', + 'vectorizeDurationMs', + ], + avg: ['fileSizeBytes', 'numChunks'], + }, + tags: [ + { source: 'indexId', as: 'index_id' }, + { source: 'embeddingModel', as: 'embedding_model' }, + { source: 'error', as: 'error' }, + ], + fields: { + items: { type: 'int', source: ['_top', 'count'] }, + chunking_duration_ms: { type: 'float', source: ['sum', 'chunkingDurationMs'] }, + embedding_duration_ms: { type: 'float', source: ['sum', 'embeddingDurationMs'] }, + file_size_bytes: { type: 'int', source: ['sum', 'fileSizeBytes'] }, + num_chunks: { type: 'int', source: ['sum', 'numChunks'] }, + total_duration_ms: { type: 'float', source: ['sum', 'totalDurationMs'] }, + total_tokens: { type: 'int', source: ['sum', 'totalTokens'] }, + vectorize_duration_ms: { type: 'float', source: ['sum', 'vectorizeDurationMs'] }, + avg_file_size_bytes: { type: 'float', source: ['avg', 'fileSizeBytes'] }, + avg_num_chunks: { type: 'float', source: ['avg', 'numChunks'] }, + }, +}; + +export const AUTO_RAG_CONFIG_API: DatasetQuery = { + key: 'auto_rag_config_api', + measurement: 'cf_auto_rag_config_api', + field: 'autoRAGConfigAPIAdaptiveGroups', + dimensions: ['datetimeMinute', 'rag', 'searchType'], + blocks: { sum: ['aiSearchCount', 'searchCount'] }, + tags: [ + { source: 'rag', as: 'rag' }, + { source: 'searchType', as: 'search_type' }, + ], + fields: { + ai_search_count: { type: 'int', source: ['sum', 'aiSearchCount'] }, + search_count: { type: 'int', source: ['sum', 'searchCount'] }, + }, +}; + +export const AUTO_RAG_ENGINE: DatasetQuery = { + key: 'auto_rag_engine', + measurement: 'cf_auto_rag_engine', + field: 'autoRAGEngineAdaptiveGroups', + dimensions: ['datetimeMinute', 'rag', 'sourceType'], + blocks: { max: ['completed', 'errored', 'queued', 'running'] }, + tags: [ + { source: 'rag', as: 'rag' }, + { source: 'sourceType', as: 'source_type' }, + ], + fields: { + completed_max: { type: 'int', source: ['max', 'completed'] }, + errored_max: { type: 'int', source: ['max', 'errored'] }, + queued_max: { type: 'int', source: ['max', 'queued'] }, + running_max: { type: 'int', source: ['max', 'running'] }, + }, +}; + +export const VECTORIZE_OPERATIONS: DatasetQuery = { + key: 'vectorize_operations', + measurement: 'cf_vectorize_operations', + field: 'vectorizeV2OperationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'indexName', 'operation', 'requestStatus'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'indexName', as: 'index_name' }, + { source: 'operation', as: 'operation' }, + { source: 'requestStatus', as: 'request_status' }, + ], + fields: { operations: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const VECTORIZE_QUERIES: DatasetQuery = { + key: 'vectorize_queries', + measurement: 'cf_vectorize_queries', + field: 'vectorizeV2QueriesAdaptiveGroups', + dimensions: ['datetimeMinute', 'indexName', 'operation', 'requestStatus'], + topLevelFields: ['count'], + blocks: { + sum: ['queriedVectorDimensions', 'requestDurationMs', 'servedVectorCount'], + avg: ['requestDurationMs'], + quantiles: ['requestDurationMsP50', 'requestDurationMsP95', 'requestDurationMsP99'], + }, + tags: [ + { source: 'indexName', as: 'index_name' }, + { source: 'operation', as: 'operation' }, + { source: 'requestStatus', as: 'request_status' }, + ], + fields: { + queries: { type: 'int', source: ['_top', 'count'] }, + queried_vector_dimensions: { type: 'int', source: ['sum', 'queriedVectorDimensions'] }, + request_duration_ms_sum: { type: 'float', source: ['sum', 'requestDurationMs'] }, + served_vectors: { type: 'int', source: ['sum', 'servedVectorCount'] }, + request_duration_ms_avg: { type: 'float', source: ['avg', 'requestDurationMs'] }, + request_duration_ms_p50: { type: 'float', source: ['quantiles', 'requestDurationMsP50'] }, + request_duration_ms_p95: { type: 'float', source: ['quantiles', 'requestDurationMsP95'] }, + request_duration_ms_p99: { type: 'float', source: ['quantiles', 'requestDurationMsP99'] }, + }, +}; + +export const VECTORIZE_STORAGE: DatasetQuery = { + key: 'vectorize_storage', + measurement: 'cf_vectorize_storage', + field: 'vectorizeV2StorageAdaptiveGroups', + dimensions: ['datetimeMinute', 'indexName'], + blocks: { max: ['storedVectorDimensions', 'vectorCount'] }, + tags: [{ source: 'indexName', as: 'index_name' }], + fields: { + stored_vector_dimensions: { type: 'int', source: ['max', 'storedVectorDimensions'] }, + vector_count: { type: 'int', source: ['max', 'vectorCount'] }, + }, +}; + +export const VECTORIZE_WRITES: DatasetQuery = { + key: 'vectorize_writes', + measurement: 'cf_vectorize_writes', + field: 'vectorizeV2WritesAdaptiveGroups', + dimensions: ['datetimeMinute', 'indexName', 'operation', 'requestStatus'], + topLevelFields: ['count'], + blocks: { + sum: ['addedVectorCount', 'deletedVectorCount', 'requestDurationMs'], + avg: ['requestDurationMs'], + quantiles: ['requestDurationMsP50', 'requestDurationMsP95', 'requestDurationMsP99'], + }, + tags: [ + { source: 'indexName', as: 'index_name' }, + { source: 'operation', as: 'operation' }, + { source: 'requestStatus', as: 'request_status' }, + ], + fields: { + writes: { type: 'int', source: ['_top', 'count'] }, + added_vectors: { type: 'int', source: ['sum', 'addedVectorCount'] }, + deleted_vectors: { type: 'int', source: ['sum', 'deletedVectorCount'] }, + request_duration_ms_sum: { type: 'float', source: ['sum', 'requestDurationMs'] }, + request_duration_ms_avg: { type: 'float', source: ['avg', 'requestDurationMs'] }, + request_duration_ms_p50: { type: 'float', source: ['quantiles', 'requestDurationMsP50'] }, + request_duration_ms_p95: { type: 'float', source: ['quantiles', 'requestDurationMsP95'] }, + request_duration_ms_p99: { type: 'float', source: ['quantiles', 'requestDurationMsP99'] }, + }, +}; + +export const BROWSER_RENDERING_API: DatasetQuery = { + key: 'browser_rendering_api', + measurement: 'cf_browser_rendering_api', + field: 'browserRenderingApiAdaptiveGroups', + dimensions: ['datetimeMinute', 'endpoint', 'status'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'endpoint', as: 'endpoint' }, + { source: 'status', as: 'status' }, + ], + fields: { requests: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const BROWSER_RENDERING_BINDING_SESSIONS: DatasetQuery = { + key: 'browser_rendering_binding_sessions', + measurement: 'cf_browser_rendering_binding_sessions', + field: 'browserRenderingBindingSessionsAdaptiveGroups', + dimensions: ['datetimeMinute'], + topLevelFields: ['count'], + blocks: { + avg: ['avgConcurrentSessions'], + max: ['maxConcurrentSessions'], + uniq: ['sessionIdCount'], + }, + tags: [], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + avg_concurrent_sessions: { type: 'float', source: ['avg', 'avgConcurrentSessions'] }, + max_concurrent_sessions: { type: 'int', source: ['max', 'maxConcurrentSessions'] }, + unique_sessions: { type: 'int', source: ['uniq', 'sessionIdCount'] }, + }, +}; + +export const BROWSER_RENDERING_BROWSER_TIME: DatasetQuery = { + key: 'browser_rendering_browser_time', + measurement: 'cf_browser_rendering_browser_time', + field: 'browserRenderingBrowserTimeUsageAdaptiveGroups', + dimensions: ['datetimeMinute'], + topLevelFields: ['count'], + blocks: { + sum: ['totalSessionDurationMs'], + avg: ['avgSessionDurationMs'], + max: ['maxSessionDurationMs'], + min: ['minSessionDurationMs'], + }, + tags: [], + fields: { + sessions: { type: 'int', source: ['_top', 'count'] }, + total_session_duration_ms: { type: 'float', source: ['sum', 'totalSessionDurationMs'] }, + avg_session_duration_ms: { type: 'float', source: ['avg', 'avgSessionDurationMs'] }, + max_session_duration_ms: { type: 'float', source: ['max', 'maxSessionDurationMs'] }, + min_session_duration_ms: { type: 'float', source: ['min', 'minSessionDurationMs'] }, + }, +}; + +export const BROWSER_RENDERING_EVENTS: DatasetQuery = { + key: 'browser_rendering_events', + measurement: 'cf_browser_rendering_events', + field: 'browserRenderingEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'scriptName', 'browserCloseReason', 'clientLibrary', 'recordingMode'], + topLevelFields: ['count'], + blocks: { + avg: ['avgConcurrentSessions'], + uniq: ['connectionIdCount', 'sessionIdCount'], + }, + tags: [ + { source: 'scriptName', as: 'script_name' }, + { source: 'browserCloseReason', as: 'browser_close_reason' }, + { source: 'clientLibrary', as: 'client_library' }, + { source: 'recordingMode', as: 'recording_mode' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + avg_concurrent_sessions: { type: 'float', source: ['avg', 'avgConcurrentSessions'] }, + unique_connections: { type: 'int', source: ['uniq', 'connectionIdCount'] }, + unique_sessions: { type: 'int', source: ['uniq', 'sessionIdCount'] }, + }, +}; + +export const BROWSER_ISOLATION_SESSIONS: DatasetQuery = { + key: 'browser_isolation_sessions', + measurement: 'cf_browser_isolation_sessions', + field: 'browserIsolationSessionsAdaptiveGroups', + dimensions: ['datetimeMinute'], + topLevelFields: ['count'], + blocks: {}, + tags: [], + fields: { sessions: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const BROWSER_ISOLATION_USER_ACTIONS: DatasetQuery = { + key: 'browser_isolation_user_actions', + measurement: 'cf_browser_isolation_user_actions', + field: 'browserIsolationUserActionsAdaptiveGroups', + dimensions: ['datetimeMinute', 'type', 'decision'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'type', as: 'action_type' }, + { source: 'decision', as: 'decision' }, + ], + fields: { actions: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const STREAM_MINUTES_VIEWED: DatasetQuery = { + key: 'stream_minutes_viewed', + measurement: 'cf_stream_minutes_viewed', + field: 'streamMinutesViewedAdaptiveGroups', + dimensions: ['datetimeMinute', 'clientCountryName', 'mediaType'], + topLevelFields: ['count'], + blocks: { sum: ['minutesViewed'] }, + tags: [ + { source: 'clientCountryName', as: 'client_country' }, + { source: 'mediaType', as: 'media_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + minutes_viewed: { type: 'float', source: ['sum', 'minutesViewed'] }, + }, +}; + +export const STREAM_CMCD: DatasetQuery = { + key: 'stream_cmcd', + measurement: 'cf_stream_cmcd', + field: 'streamCMCDAdaptiveGroups', + dimensions: ['datetimeMinute', 'streamType', 'streamingFormat', 'country'], + topLevelFields: ['count'], + blocks: { + sum: ['millisecondsViewed'], + avg: [ + 'bufferLength', + 'bufferStarvationDuration', + 'encodedBitrate', + 'initialBufferStarvationDuration', + 'measuredThroughput', + ], + uniq: ['viewers'], + }, + tags: [ + { source: 'streamType', as: 'stream_type' }, + { source: 'streamingFormat', as: 'streaming_format' }, + { source: 'country', as: 'country' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + milliseconds_viewed: { type: 'int', source: ['sum', 'millisecondsViewed'] }, + avg_buffer_length: { type: 'float', source: ['avg', 'bufferLength'] }, + avg_buffer_starvation_duration: { type: 'float', source: ['avg', 'bufferStarvationDuration'] }, + avg_encoded_bitrate: { type: 'float', source: ['avg', 'encodedBitrate'] }, + avg_initial_buffer_starvation_duration: { type: 'float', source: ['avg', 'initialBufferStarvationDuration'] }, + avg_measured_throughput: { type: 'float', source: ['avg', 'measuredThroughput'] }, + unique_viewers: { type: 'int', source: ['uniq', 'viewers'] }, + }, +}; + +export const VIDEO_BUFFER_EVENTS: DatasetQuery = { + key: 'video_buffer_events', + measurement: 'cf_video_buffer_events', + field: 'videoBufferEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'clientCountryName', 'deviceType'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'clientCountryName', as: 'client_country' }, + { source: 'deviceType', as: 'device_type' }, + ], + fields: { events: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const VIDEO_PLAYBACK_EVENTS: DatasetQuery = { + key: 'video_playback_events', + measurement: 'cf_video_playback_events', + field: 'videoPlaybackEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'clientCountryName', 'deviceType'], + topLevelFields: ['count'], + blocks: { sum: ['timeViewedMinutes'] }, + tags: [ + { source: 'clientCountryName', as: 'client_country' }, + { source: 'deviceType', as: 'device_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + time_viewed_minutes: { type: 'float', source: ['sum', 'timeViewedMinutes'] }, + }, +}; + +export const VIDEO_QUALITY_EVENTS: DatasetQuery = { + key: 'video_quality_events', + measurement: 'cf_video_quality_events', + field: 'videoQualityEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'clientCountryName', 'deviceType', 'qualityResolution'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'clientCountryName', as: 'client_country' }, + { source: 'deviceType', as: 'device_type' }, + { source: 'qualityResolution', as: 'quality_resolution' }, + ], + fields: { events: { type: 'int', source: ['_top', 'count'] } }, +}; + +export const LIVE_INPUT_EVENTS: DatasetQuery = { + key: 'live_input_events', + measurement: 'cf_live_input_events', + field: 'liveInputEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'inputId', 'eventCode'], + topLevelFields: ['count'], + blocks: { + avg: ['bitRateMinute', 'gopByteSize', 'gopDuration', 'gopUploadTime', 'uploadDurationRatio'], + max: ['gopByteSize', 'gopDuration', 'gopUploadTime', 'uploadDurationRatio'], + }, + tags: [ + { source: 'inputId', as: 'input_id' }, + { source: 'eventCode', as: 'event_code' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + avg_bit_rate: { type: 'float', source: ['avg', 'bitRateMinute'] }, + avg_gop_byte_size: { type: 'float', source: ['avg', 'gopByteSize'] }, + avg_gop_duration: { type: 'float', source: ['avg', 'gopDuration'] }, + avg_gop_upload_time: { type: 'float', source: ['avg', 'gopUploadTime'] }, + avg_upload_duration_ratio: { type: 'float', source: ['avg', 'uploadDurationRatio'] }, + max_gop_byte_size: { type: 'float', source: ['max', 'gopByteSize'] }, + max_gop_duration: { type: 'float', source: ['max', 'gopDuration'] }, + max_gop_upload_time: { type: 'float', source: ['max', 'gopUploadTime'] }, + }, +}; + +export const REALTIME_KIT_USAGE: DatasetQuery = { + key: 'realtime_kit_usage', + measurement: 'cf_realtime_kit_usage', + field: 'realtimeKitUsageAdaptiveGroups', + dimensions: ['datetimeMinute', 'appId'], + topLevelFields: ['count'], + blocks: { + sum: ['audioMinutes', 'exportAudioMinutes', 'exportMinutes', 'mediaMinutes'], + }, + tags: [{ source: 'appId', as: 'app_id' }], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + audio_minutes: { type: 'float', source: ['sum', 'audioMinutes'] }, + export_audio_minutes: { type: 'float', source: ['sum', 'exportAudioMinutes'] }, + export_minutes: { type: 'float', source: ['sum', 'exportMinutes'] }, + media_minutes: { type: 'float', source: ['sum', 'mediaMinutes'] }, + }, +}; + +export const CALLS_USAGE: DatasetQuery = { + key: 'calls_usage', + measurement: 'cf_calls_usage', + field: 'callsUsageAdaptiveGroups', + dimensions: ['datetimeMinute', 'appId', 'trackType'], + blocks: { sum: ['egressBytes', 'ingressBytes'] }, + tags: [ + { source: 'appId', as: 'app_id' }, + { source: 'trackType', as: 'track_type' }, + ], + fields: { + egress_bytes: { type: 'int', source: ['sum', 'egressBytes'] }, + ingress_bytes: { type: 'int', source: ['sum', 'ingressBytes'] }, + }, +}; + +export const CALLS_TURN_USAGE: DatasetQuery = { + key: 'calls_turn_usage', + measurement: 'cf_calls_turn_usage', + field: 'callsTurnUsageAdaptiveGroups', + dimensions: ['datetimeMinute', 'datacenterCode', 'datacenterCountry'], + blocks: { + sum: ['egressBytes', 'ingressBytes'], + avg: ['concurrentConnectionsMinute'], + }, + tags: [ + { source: 'datacenterCode', as: 'datacenter_code' }, + { source: 'datacenterCountry', as: 'datacenter_country' }, + ], + fields: { + egress_bytes: { type: 'int', source: ['sum', 'egressBytes'] }, + ingress_bytes: { type: 'int', source: ['sum', 'ingressBytes'] }, + avg_concurrent_connections: { type: 'float', source: ['avg', 'concurrentConnectionsMinute'] }, + }, +}; + +export const IMAGES_REQUESTS: DatasetQuery = { + key: 'images_requests', + measurement: 'cf_images_requests', + field: 'imagesRequestsAdaptiveGroups', + dimensions: ['datetimeMinute'], + blocks: { + sum: ['requests'], + avg: ['sampleInterval'], + }, + tags: [], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const TO_MARKDOWN: DatasetQuery = { + key: 'to_markdown', + measurement: 'cf_to_markdown', + field: 'toMarkdownConversionAdaptiveGroups', + dimensions: ['datetimeMinute', 'result', 'service', 'source', 'mimeType', 'errorReason'], + topLevelFields: ['count'], + blocks: { + avg: ['completed', 'durationMs', 'fileSize'], + max: ['durationMs', 'fileSize'], + min: ['durationMs', 'fileSize'], + }, + tags: [ + { source: 'result', as: 'result' }, + { source: 'service', as: 'service' }, + { source: 'source', as: 'source' }, + { source: 'mimeType', as: 'mime_type' }, + { source: 'errorReason', as: 'error_reason' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + avg_completed: { type: 'float', source: ['avg', 'completed'] }, + avg_duration_ms: { type: 'float', source: ['avg', 'durationMs'] }, + avg_file_size: { type: 'float', source: ['avg', 'fileSize'] }, + max_duration_ms: { type: 'float', source: ['max', 'durationMs'] }, + max_file_size: { type: 'float', source: ['max', 'fileSize'] }, + min_duration_ms: { type: 'float', source: ['min', 'durationMs'] }, + min_file_size: { type: 'float', source: ['min', 'fileSize'] }, + }, +}; + +export const RUM_PAGELOAD: DatasetQuery = { + key: 'rum_pageload', + measurement: 'cf_rum_pageload', + field: 'rumPageloadEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'siteTag', 'bot', 'countryName', 'deviceType'], + topLevelFields: ['count'], + blocks: { + sum: ['visits'], + avg: ['sampleInterval'], + }, + tags: [ + { source: 'siteTag', as: 'site_tag' }, + { source: 'bot', as: 'bot' }, + { source: 'countryName', as: 'country' }, + { source: 'deviceType', as: 'device_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + visits: { type: 'int', source: ['sum', 'visits'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const RUM_PERFORMANCE: DatasetQuery = { + key: 'rum_performance', + measurement: 'cf_rum_performance', + field: 'rumPerformanceEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'siteTag', 'countryName', 'deviceType'], + topLevelFields: ['count'], + blocks: { + sum: ['visits'], + avg: [ + 'connectionTime', + 'dnsTime', + 'firstContentfulPaint', + 'firstPaint', + 'loadEventTime', + 'pageLoadTime', + 'pageRenderTime', + 'requestTime', + 'responseTime', + ], + quantiles: [ + 'pageLoadTimeP50', + 'pageLoadTimeP95', + 'pageLoadTimeP99', + 'firstContentfulPaintP50', + 'firstContentfulPaintP95', + 'firstContentfulPaintP99', + ], + }, + tags: [ + { source: 'siteTag', as: 'site_tag' }, + { source: 'countryName', as: 'country' }, + { source: 'deviceType', as: 'device_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + visits: { type: 'int', source: ['sum', 'visits'] }, + avg_connection_time_ms: { type: 'float', source: ['avg', 'connectionTime'] }, + avg_dns_time_ms: { type: 'float', source: ['avg', 'dnsTime'] }, + avg_first_contentful_paint_ms: { type: 'float', source: ['avg', 'firstContentfulPaint'] }, + avg_first_paint_ms: { type: 'float', source: ['avg', 'firstPaint'] }, + avg_load_event_time_ms: { type: 'float', source: ['avg', 'loadEventTime'] }, + avg_page_load_time_ms: { type: 'float', source: ['avg', 'pageLoadTime'] }, + avg_page_render_time_ms: { type: 'float', source: ['avg', 'pageRenderTime'] }, + avg_request_time_ms: { type: 'float', source: ['avg', 'requestTime'] }, + avg_response_time_ms: { type: 'float', source: ['avg', 'responseTime'] }, + p50_page_load_time_ms: { type: 'float', source: ['quantiles', 'pageLoadTimeP50'] }, + p95_page_load_time_ms: { type: 'float', source: ['quantiles', 'pageLoadTimeP95'] }, + p99_page_load_time_ms: { type: 'float', source: ['quantiles', 'pageLoadTimeP99'] }, + p50_fcp_ms: { type: 'float', source: ['quantiles', 'firstContentfulPaintP50'] }, + p95_fcp_ms: { type: 'float', source: ['quantiles', 'firstContentfulPaintP95'] }, + p99_fcp_ms: { type: 'float', source: ['quantiles', 'firstContentfulPaintP99'] }, + }, +}; + +// RUM Web Vitals has a 30-field-per-request Cloudflare cap, so we only select +// the total visit counters plus the average and p75/p95 quantile values. +// "good/needs-improvement/poor" buckets are omitted to stay under the limit; +// the averages and quantiles carry the same story more compactly. +export const RUM_WEB_VITALS: DatasetQuery = { + key: 'rum_web_vitals', + measurement: 'cf_rum_web_vitals', + field: 'rumWebVitalsEventsAdaptiveGroups', + dimensions: ['datetimeMinute', 'siteTag', 'countryName', 'deviceType'], + topLevelFields: ['count'], + blocks: { + sum: ['visits', 'clsTotal', 'fcpTotal', 'fidTotal', 'inpTotal', 'lcpTotal', 'ttfbTotal'], + avg: [ + 'cumulativeLayoutShift', + 'firstContentfulPaint', + 'firstInputDelay', + 'interactionToNextPaint', + 'largestContentfulPaint', + 'timeToFirstByte', + ], + quantiles: [ + 'largestContentfulPaintP75', + 'largestContentfulPaintP95', + 'cumulativeLayoutShiftP75', + 'cumulativeLayoutShiftP95', + 'interactionToNextPaintP75', + 'interactionToNextPaintP95', + ], + }, + tags: [ + { source: 'siteTag', as: 'site_tag' }, + { source: 'countryName', as: 'country' }, + { source: 'deviceType', as: 'device_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + visits: { type: 'int', source: ['sum', 'visits'] }, + cls_total: { type: 'int', source: ['sum', 'clsTotal'] }, + fcp_total: { type: 'int', source: ['sum', 'fcpTotal'] }, + fid_total: { type: 'int', source: ['sum', 'fidTotal'] }, + inp_total: { type: 'int', source: ['sum', 'inpTotal'] }, + lcp_total: { type: 'int', source: ['sum', 'lcpTotal'] }, + ttfb_total: { type: 'int', source: ['sum', 'ttfbTotal'] }, + avg_cls: { type: 'float', source: ['avg', 'cumulativeLayoutShift'] }, + avg_fcp_ms: { type: 'float', source: ['avg', 'firstContentfulPaint'] }, + avg_fid_ms: { type: 'float', source: ['avg', 'firstInputDelay'] }, + avg_inp_ms: { type: 'float', source: ['avg', 'interactionToNextPaint'] }, + avg_lcp_ms: { type: 'float', source: ['avg', 'largestContentfulPaint'] }, + avg_ttfb_ms: { type: 'float', source: ['avg', 'timeToFirstByte'] }, + p75_lcp_ms: { type: 'float', source: ['quantiles', 'largestContentfulPaintP75'] }, + p95_lcp_ms: { type: 'float', source: ['quantiles', 'largestContentfulPaintP95'] }, + p75_cls: { type: 'float', source: ['quantiles', 'cumulativeLayoutShiftP75'] }, + p95_cls: { type: 'float', source: ['quantiles', 'cumulativeLayoutShiftP95'] }, + p75_inp_ms: { type: 'float', source: ['quantiles', 'interactionToNextPaintP75'] }, + p95_inp_ms: { type: 'float', source: ['quantiles', 'interactionToNextPaintP95'] }, + }, +}; + +export const PIPELINES_INGESTION: DatasetQuery = { + key: 'pipelines_ingestion', + measurement: 'cf_pipelines_ingestion', + field: 'pipelinesIngestionAdaptiveGroups', + dimensions: ['datetimeMinute', 'pipelineId'], + topLevelFields: ['count'], + blocks: { sum: ['ingestedBytes', 'ingestedRecords'] }, + tags: [{ source: 'pipelineId', as: 'pipeline_id' }], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + ingested_bytes: { type: 'int', source: ['sum', 'ingestedBytes'] }, + ingested_records: { type: 'int', source: ['sum', 'ingestedRecords'] }, + }, +}; + +export const PIPELINES_DELIVERY: DatasetQuery = { + key: 'pipelines_delivery', + measurement: 'cf_pipelines_delivery', + field: 'pipelinesDeliveryAdaptiveGroups', + dimensions: ['datetimeMinute', 'pipelineId'], + topLevelFields: ['count'], + blocks: { sum: ['deliveredBytes'] }, + tags: [{ source: 'pipelineId', as: 'pipeline_id' }], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + delivered_bytes: { type: 'int', source: ['sum', 'deliveredBytes'] }, + }, +}; + +export const PIPELINES_OPERATOR: DatasetQuery = { + key: 'pipelines_operator', + measurement: 'cf_pipelines_operator', + field: 'pipelinesOperatorAdaptiveGroups', + dimensions: ['datetimeMinute', 'pipelineId', 'streamId'], + blocks: { sum: ['bytesIn', 'decodeErrors', 'recordsIn'] }, + tags: [ + { source: 'pipelineId', as: 'pipeline_id' }, + { source: 'streamId', as: 'stream_id' }, + ], + fields: { + bytes_in: { type: 'int', source: ['sum', 'bytesIn'] }, + decode_errors: { type: 'int', source: ['sum', 'decodeErrors'] }, + records_in: { type: 'int', source: ['sum', 'recordsIn'] }, + }, +}; + +export const PIPELINES_SINK: DatasetQuery = { + key: 'pipelines_sink', + measurement: 'cf_pipelines_sink', + field: 'pipelinesSinkAdaptiveGroups', + dimensions: ['datetimeMinute', 'pipelineId', 'sinkId'], + blocks: { + sum: ['bytesWritten', 'filesWritten', 'recordsWritten', 'rowGroupsWritten', 'uncompressedBytesWritten'], + }, + tags: [ + { source: 'pipelineId', as: 'pipeline_id' }, + { source: 'sinkId', as: 'sink_id' }, + ], + fields: { + bytes_written: { type: 'int', source: ['sum', 'bytesWritten'] }, + files_written: { type: 'int', source: ['sum', 'filesWritten'] }, + records_written: { type: 'int', source: ['sum', 'recordsWritten'] }, + row_groups_written: { type: 'int', source: ['sum', 'rowGroupsWritten'] }, + uncompressed_bytes_written: { type: 'int', source: ['sum', 'uncompressedBytesWritten'] }, + }, +}; + +export const CONTAINERS: DatasetQuery = { + key: 'containers', + measurement: 'cf_containers', + field: 'containersMetricsAdaptiveGroups', + dimensions: ['datetimeMinute', 'applicationId', 'region', 'procType'], + topLevelFields: ['count'], + blocks: { + sum: ['allocatedCpu', 'allocatedDisk', 'allocatedMemory', 'containerUptime', 'cpuTimeSec', 'rxBytes', 'txBytes'], + avg: ['cpuLoad', 'cpuUtilization', 'memory'], + max: ['cpuLoad', 'cpuUtilization', 'diskUsage', 'diskUsagePercentage', 'memory'], + }, + tags: [ + { source: 'applicationId', as: 'application_id' }, + { source: 'region', as: 'region' }, + { source: 'procType', as: 'proc_type' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + allocated_cpu: { type: 'int', source: ['sum', 'allocatedCpu'] }, + allocated_disk: { type: 'int', source: ['sum', 'allocatedDisk'] }, + allocated_memory: { type: 'int', source: ['sum', 'allocatedMemory'] }, + container_uptime_sum: { type: 'int', source: ['sum', 'containerUptime'] }, + cpu_time_sec: { type: 'float', source: ['sum', 'cpuTimeSec'] }, + rx_bytes: { type: 'int', source: ['sum', 'rxBytes'] }, + tx_bytes: { type: 'int', source: ['sum', 'txBytes'] }, + avg_cpu_load: { type: 'float', source: ['avg', 'cpuLoad'] }, + avg_cpu_utilization: { type: 'float', source: ['avg', 'cpuUtilization'] }, + avg_memory: { type: 'float', source: ['avg', 'memory'] }, + max_cpu_load: { type: 'float', source: ['max', 'cpuLoad'] }, + max_cpu_utilization: { type: 'float', source: ['max', 'cpuUtilization'] }, + max_disk_usage: { type: 'float', source: ['max', 'diskUsage'] }, + max_disk_usage_percentage: { type: 'float', source: ['max', 'diskUsagePercentage'] }, + max_memory: { type: 'float', source: ['max', 'memory'] }, + }, +}; + +export const TURNSTILE: DatasetQuery = { + key: 'turnstile', + measurement: 'cf_turnstile', + field: 'turnstileAdaptiveGroups', + dimensions: ['datetimeMinute', 'action', 'eventType', 'siteKey', 'countryCode'], + topLevelFields: ['count'], + blocks: { avg: ['sampleInterval'] }, + tags: [ + { source: 'action', as: 'action' }, + { source: 'eventType', as: 'event_type' }, + { source: 'siteKey', as: 'site_key' }, + { source: 'countryCode', as: 'country_code' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const SIPPY_OPERATIONS: DatasetQuery = { + key: 'sippy_operations', + measurement: 'cf_sippy_operations', + field: 'sippyOperationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'action', 'status', 'bucket', 'target'], + topLevelFields: ['count'], + blocks: { sum: ['size'] }, + tags: [ + { source: 'action', as: 'action' }, + { source: 'status', as: 'status' }, + { source: 'bucket', as: 'bucket' }, + { source: 'target', as: 'target' }, + ], + fields: { + operations: { type: 'int', source: ['_top', 'count'] }, + size_bytes: { type: 'int', source: ['sum', 'size'] }, + }, +}; + +// ============================================================================= +// Additional zone-scope datasets. These are iterated per-zone by the collector +// (one subrequest per zone per tick each), so conservative dimensions are +// especially important here. +// ============================================================================= + +export const API_GATEWAY_SESSIONS: DatasetQuery = { + key: 'api_gateway_sessions', + measurement: 'cf_api_gateway_sessions', + field: 'apiGatewayMatchedSessionIDsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'apiGatewayMatchedSessionIdentifierType'], + topLevelFields: ['count'], + blocks: { avg: ['sampleInterval'] }, + tags: [{ source: 'apiGatewayMatchedSessionIdentifierType', as: 'identifier_type' }], + fields: { + matched_sessions: { type: 'int', source: ['_top', 'count'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const CACHE_RESERVE_OPERATIONS: DatasetQuery = { + key: 'cache_reserve_operations', + measurement: 'cf_cache_reserve_operations', + field: 'cacheReserveOperationsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'actionStatus', 'operationClass'], + blocks: { sum: ['requests'] }, + tags: [ + { source: 'actionStatus', as: 'action_status' }, + { source: 'operationClass', as: 'operation_class' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + }, +}; + +export const CACHE_RESERVE_STORAGE: DatasetQuery = { + key: 'cache_reserve_storage', + measurement: 'cf_cache_reserve_storage', + field: 'cacheReserveStorageAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'bucketName'], + blocks: { max: ['objectCount', 'storedBytes'] }, + tags: [{ source: 'bucketName', as: 'bucket_name' }], + fields: { + object_count: { type: 'int', source: ['max', 'objectCount'] }, + stored_bytes: { type: 'int', source: ['max', 'storedBytes'] }, + }, +}; + +export const DMARC_REPORTS: DatasetQuery = { + key: 'dmarc_reports', + measurement: 'cf_dmarc_reports', + field: 'dmarcReportsSourcesAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'disposition', 'dkim', 'spf'], + blocks: { + sum: ['dkimPass', 'dmarc', 'spfPass', 'totalMatchingMessages'], + uniq: ['ipCount'], + }, + tags: [ + { source: 'disposition', as: 'disposition' }, + { source: 'dkim', as: 'dkim' }, + { source: 'spf', as: 'spf' }, + ], + fields: { + messages: { type: 'int', source: ['sum', 'totalMatchingMessages'] }, + dmarc_pass: { type: 'int', source: ['sum', 'dmarc'] }, + dkim_pass: { type: 'int', source: ['sum', 'dkimPass'] }, + spf_pass: { type: 'int', source: ['sum', 'spfPass'] }, + unique_ips: { type: 'int', source: ['uniq', 'ipCount'] }, + }, +}; + +export const DNS_ANALYTICS: DatasetQuery = { + key: 'dns_analytics', + measurement: 'cf_dns_analytics', + field: 'dnsAnalyticsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'queryType', 'responseCode', 'responseCached'], + topLevelFields: ['count'], + blocks: { + sum: ['countNotCachedAndNotStale', 'countStale'], + avg: ['processingTimeUs', 'sampleInterval'], + }, + tags: [ + { source: 'queryType', as: 'query_type' }, + { source: 'responseCode', as: 'response_code' }, + { source: 'responseCached', as: 'response_cached' }, + ], + fields: { + queries: { type: 'int', source: ['_top', 'count'] }, + queries_uncached: { type: 'int', source: ['sum', 'countNotCachedAndNotStale'] }, + queries_stale: { type: 'int', source: ['sum', 'countStale'] }, + processing_time_us_avg: { type: 'float', source: ['avg', 'processingTimeUs'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const EMAIL_ROUTING: DatasetQuery = { + key: 'email_routing', + measurement: 'cf_email_routing', + field: 'emailRoutingAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'action', 'status', 'dkim', 'dmarc', 'spf'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'action', as: 'action' }, + { source: 'status', as: 'status' }, + { source: 'dkim', as: 'dkim' }, + { source: 'dmarc', as: 'dmarc' }, + { source: 'spf', as: 'spf' }, + ], + fields: { + messages: { type: 'int', source: ['_top', 'count'] }, + }, +}; + +export const EMAIL_SENDING: DatasetQuery = { + key: 'email_sending', + measurement: 'cf_email_sending', + field: 'emailSendingAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'eventType', 'status', 'dkim', 'dmarc', 'spf'], + topLevelFields: ['count'], + blocks: {}, + tags: [ + { source: 'eventType', as: 'event_type' }, + { source: 'status', as: 'status' }, + { source: 'dkim', as: 'dkim' }, + { source: 'dmarc', as: 'dmarc' }, + { source: 'spf', as: 'spf' }, + ], + fields: { + messages: { type: 'int', source: ['_top', 'count'] }, + }, +}; + +export const LOGPUSH_HEALTH: DatasetQuery = { + key: 'logpush_health', + measurement: 'cf_logpush_health', + field: 'logpushHealthAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'destinationType', 'status', 'final', 'success', 'jobId'], + topLevelFields: ['count'], + blocks: { + sum: ['bytes', 'bytesCompressed', 'records', 'uploads'], + avg: ['uploadDuration', 'sampleInterval'], + }, + tags: [ + { source: 'destinationType', as: 'destination_type' }, + { source: 'status', as: 'status' }, + { source: 'final', as: 'final' }, + { source: 'success', as: 'success' }, + { source: 'jobId', as: 'job_id' }, + ], + fields: { + events: { type: 'int', source: ['_top', 'count'] }, + bytes: { type: 'int', source: ['sum', 'bytes'] }, + bytes_compressed: { type: 'int', source: ['sum', 'bytesCompressed'] }, + records: { type: 'int', source: ['sum', 'records'] }, + uploads: { type: 'int', source: ['sum', 'uploads'] }, + upload_duration_avg: { type: 'float', source: ['avg', 'uploadDuration'] }, + sample_interval: { type: 'float', source: ['avg', 'sampleInterval'] }, + }, +}; + +export const WORKERS_ZONE_INVOCATIONS: DatasetQuery = { + key: 'workers_zone_invocations', + measurement: 'cf_workers_zone_invocations', + field: 'workersZoneInvocationsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'constantScriptId', 'httpResponseStatus', 'status'], + blocks: { + sum: ['requests', 'responseBodySize', 'subrequests', 'totalCpuTime'], + avg: ['avgCpuTime'], + }, + tags: [ + { source: 'constantScriptId', as: 'script_id' }, + { source: 'httpResponseStatus', as: 'http_status' }, + { source: 'status', as: 'status' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + response_body_size: { type: 'int', source: ['sum', 'responseBodySize'] }, + subrequests: { type: 'int', source: ['sum', 'subrequests'] }, + total_cpu_time_us: { type: 'float', source: ['sum', 'totalCpuTime'] }, + avg_cpu_time_us: { type: 'float', source: ['avg', 'avgCpuTime'] }, + }, +}; + +export const WORKERS_ZONE_SUBREQUESTS: DatasetQuery = { + key: 'workers_zone_subrequests', + measurement: 'cf_workers_zone_subrequests', + field: 'workersZoneSubrequestsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute', 'cacheStatus', 'httpResponseStatus'], + blocks: { + sum: ['requestBodySize', 'requestBodySizeUncached', 'responseBodySize', 'subrequests'], + }, + tags: [ + { source: 'cacheStatus', as: 'cache_status' }, + { source: 'httpResponseStatus', as: 'http_status' }, + ], + fields: { + subrequests: { type: 'int', source: ['sum', 'subrequests'] }, + request_body_size: { type: 'int', source: ['sum', 'requestBodySize'] }, + request_body_size_uncached: { type: 'int', source: ['sum', 'requestBodySizeUncached'] }, + response_body_size: { type: 'int', source: ['sum', 'responseBodySize'] }, + }, +}; + +// Note: `cdnNetworkAnalyticsAdaptiveGroups` (network-layer CDN analytics) is +// plan-gated on Enterprise and returns `authz` on our plan. Zaraz (`zarazTrack*`, +// `zarazTriggers*`) uses a different filter input shape (`datetimeMinute_geq` +// instead of `datetime_geq`) that doesn't compose with our batched query +// pattern — skipped pending either adoption of Zaraz or the product being EOL'd. +// `cloudchamberMetricsAdaptiveGroups` is a duplicate schema of `containersMetrics` +// — we wire up containers only. +// +// Zone-scope datasets skipped as plan-gated on our account (return `does not +// have access to the path`): `cacheReserveRequestsAdaptiveGroups`, +// `healthCheckEventsAdaptiveGroups`, `loadBalancingRequestsAdaptiveGroups`, +// `nelReportsAdaptiveGroups`, `pageShieldReportsAdaptiveGroups`, +// `waitingRoomAnalyticsAdaptiveGroups`, `firewallEventsAdaptiveGroups`. + +export const ALL_DATASETS: DatasetQuery[] = [ + WORKERS_INVOCATIONS, + WORKERS_SUBREQUESTS, + WORKERS_OVERVIEW, + WORKERS_ANALYTICS_ENGINE, + WORKERS_BUILDS, + WORKERS_VPC, + WORKER_PLACEMENT, + WORKFLOWS, + D1_QUERIES, + D1_QUERIES_DETAIL, + D1_STORAGE, + R2_OPERATIONS, + R2_STORAGE, + SIPPY_OPERATIONS, + KV_OPERATIONS, + KV_STORAGE, + DURABLE_OBJECTS_INVOCATIONS, + DURABLE_OBJECTS_PERIODIC, + DURABLE_OBJECTS_STORAGE, + DURABLE_OBJECTS_SQL_STORAGE, + DURABLE_OBJECTS_SUBREQUESTS, + QUEUE_OPERATIONS, + QUEUE_BACKLOG, + QUEUE_CONSUMER, + HYPERDRIVE_QUERIES, + HYPERDRIVE_POOL, + HTTP_REQUESTS_OVERVIEW, + HTTP_REQUESTS_DETAIL, + API_GATEWAY_SESSIONS, + CACHE_RESERVE_OPERATIONS, + CACHE_RESERVE_STORAGE, + DMARC_REPORTS, + DNS_ANALYTICS, + EMAIL_ROUTING, + EMAIL_SENDING, + LOGPUSH_HEALTH, + WORKERS_ZONE_INVOCATIONS, + WORKERS_ZONE_SUBREQUESTS, + PAGES_FUNCTIONS_INVOCATIONS, + AI_GATEWAY_REQUESTS, + AI_GATEWAY_CACHE, + AI_GATEWAY_ERRORS, + AI_GATEWAY_SIZE, + AI_INFERENCE, + AI_SEARCH_API, + AI_SEARCH_INGESTED_ITEMS, + AUTO_RAG_CONFIG_API, + AUTO_RAG_ENGINE, + VECTORIZE_OPERATIONS, + VECTORIZE_QUERIES, + VECTORIZE_STORAGE, + VECTORIZE_WRITES, + BROWSER_RENDERING_API, + BROWSER_RENDERING_BINDING_SESSIONS, + BROWSER_RENDERING_BROWSER_TIME, + BROWSER_RENDERING_EVENTS, + BROWSER_ISOLATION_SESSIONS, + BROWSER_ISOLATION_USER_ACTIONS, + STREAM_MINUTES_VIEWED, + STREAM_CMCD, + VIDEO_BUFFER_EVENTS, + VIDEO_PLAYBACK_EVENTS, + VIDEO_QUALITY_EVENTS, + LIVE_INPUT_EVENTS, + REALTIME_KIT_USAGE, + CALLS_USAGE, + CALLS_TURN_USAGE, + IMAGES_REQUESTS, + TO_MARKDOWN, + RUM_PAGELOAD, + RUM_PERFORMANCE, + RUM_WEB_VITALS, + PIPELINES_INGESTION, + PIPELINES_DELIVERY, + PIPELINES_OPERATOR, + PIPELINES_SINK, + CONTAINERS, + TURNSTILE, +]; diff --git a/apps/cloudflare-metrics/src/deferred.ts b/apps/cloudflare-metrics/src/deferred.ts new file mode 100644 index 0000000..d2eaa05 --- /dev/null +++ b/apps/cloudflare-metrics/src/deferred.ts @@ -0,0 +1,14 @@ +export class DeferredRepository { + private deferred: (() => Promise)[] = []; + constructor(private ctx: ExecutionContext) {} + + defer(call: () => Promise): void { + this.deferred.push(call); + } + + runDeferred() { + for (const call of this.deferred) { + this.ctx.waitUntil(call()); + } + } +} diff --git a/apps/cloudflare-metrics/src/emit.test.ts b/apps/cloudflare-metrics/src/emit.test.ts new file mode 100644 index 0000000..bb3137c --- /dev/null +++ b/apps/cloudflare-metrics/src/emit.test.ts @@ -0,0 +1,255 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { buildMetric, normalizeTagValue, resolveTimestamp } from './emit.js'; +import { __resetResourceCachesForTests, type ResourceCache } from './resource-cache.js'; +import type { DatasetQuery, DatasetRow } from './types.js'; + +function emptyCache(): ResourceCache { + return { + d1Databases: new Map(), + queues: new Map(), + zones: new Map(), + bulkZoneTags: new Set(), + }; +} + +const WORKERS_DATASET: DatasetQuery = { + key: 'workers_invocations', + measurement: 'cf_workers_invocations', + field: 'workersInvocationsAdaptive', + dimensions: ['datetimeMinute', 'scriptName', 'status'], + topLevelFields: [], + blocks: { sum: ['requests', 'errors'] }, + tags: [ + { source: 'scriptName', as: 'script_name' }, + { source: 'status', as: 'status' }, + ], + fields: { + requests: { type: 'int', source: ['sum', 'requests'] }, + errors: { type: 'int', source: ['sum', 'errors'] }, + }, +}; + +describe('resolveTimestamp', () => { + it('parses datetimeMinute ISO string', () => { + const row: DatasetRow = { dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' } }; + const result = resolveTimestamp(WORKERS_DATASET, row); + expect(result).toEqual(new Date('2026-04-10T12:00:00Z')); + }); + + it('returns null when dimension is missing', () => { + const row: DatasetRow = { dimensions: {} }; + expect(resolveTimestamp(WORKERS_DATASET, row)).toBeNull(); + }); + + it('returns null when dimension value is null', () => { + const row: DatasetRow = { dimensions: { datetimeMinute: null } }; + expect(resolveTimestamp(WORKERS_DATASET, row)).toBeNull(); + }); + + it('returns null for invalid date strings', () => { + const row: DatasetRow = { dimensions: { datetimeMinute: 'not-a-date' } }; + expect(resolveTimestamp(WORKERS_DATASET, row)).toBeNull(); + }); + + it('converts date dimension to UTC midnight', () => { + const dataset: DatasetQuery = { + ...WORKERS_DATASET, + timestampDimension: 'date', + }; + const row: DatasetRow = { dimensions: { date: '2026-04-10' } }; + const result = resolveTimestamp(dataset, row); + expect(result).toEqual(new Date('2026-04-10T00:00:00Z')); + }); + + it('coerces non-string values to string', () => { + const row: DatasetRow = { dimensions: { datetimeMinute: 1_744_272_000_000 as unknown as string } }; + expect(resolveTimestamp(WORKERS_DATASET, row)).toBeNull(); + }); +}); + +describe('normalizeTagValue', () => { + it('returns string values as-is', () => { + expect(normalizeTagValue('hello')).toBe('hello'); + }); + + it('returns undefined for empty strings', () => { + expect(normalizeTagValue('')).toBeUndefined(); + }); + + it('returns undefined for null', () => { + expect(normalizeTagValue(null)).toBeUndefined(); + }); + + it('returns undefined for undefined', () => { + expect(normalizeTagValue()).toBeUndefined(); + }); + + it('coerces numbers to strings', () => { + expect(normalizeTagValue(200)).toBe('200'); + expect(normalizeTagValue(0)).toBe('0'); + }); + + it('coerces booleans to strings', () => { + expect(normalizeTagValue(true)).toBe('true'); + expect(normalizeTagValue(false)).toBe('false'); + }); + + it('returns undefined for objects', () => { + expect(normalizeTagValue({})).toBeUndefined(); + expect(normalizeTagValue([])).toBeUndefined(); + }); +}); + +describe('buildMetric', () => { + beforeEach(() => { + __resetResourceCachesForTests(); + }); + + it('builds a metric with tags and fields from a row', () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', scriptName: 'my-worker', status: 'success' }, + sum: { requests: 10, errors: 0 }, + }; + const metric = buildMetric(WORKERS_DATASET, row, 'acct-1', emptyCache()); + expect(metric).not.toBeNull(); + expect(metric!.name).toBe('cf_workers_invocations'); + expect(metric!.tags.get('account_id')).toBe('acct-1'); + expect(metric!.tags.get('script_name')).toBe('my-worker'); + expect(metric!.tags.get('status')).toBe('success'); + expect(metric!.fields.get('requests')).toEqual({ value: 10, type: 'int' }); + expect(metric!.fields.get('errors')).toEqual({ value: 0, type: 'int' }); + expect(metric!.exportTimestamp).toEqual(new Date('2026-04-10T12:00:00Z')); + }); + + it('returns null when timestamp is missing', () => { + const row: DatasetRow = { dimensions: {}, sum: { requests: 1 } }; + expect(buildMetric(WORKERS_DATASET, row, 'acct', emptyCache())).toBeNull(); + }); + + it('returns null when no numeric fields are present', () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, + sum: { requests: null, errors: null }, + }; + expect(buildMetric(WORKERS_DATASET, row, 'acct', emptyCache())).toBeNull(); + }); + + it('skips null fields but keeps zero-valued fields', () => { + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', scriptName: 'w', status: 'ok' }, + sum: { requests: 0, errors: null }, + }; + const metric = buildMetric(WORKERS_DATASET, row, 'acct', emptyCache()); + expect(metric).not.toBeNull(); + expect(metric!.fields.has('requests')).toBe(true); + expect(metric!.fields.has('errors')).toBe(false); + }); + + it('applies float type and scale factor', () => { + const dataset: DatasetQuery = { + ...WORKERS_DATASET, + fields: { + duration_ms: { type: 'float', source: ['sum', 'duration'], scale: 1000 }, + }, + }; + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, + sum: { duration: 0.5 }, + }; + const metric = buildMetric(dataset, row, 'acct', emptyCache()); + expect(metric!.fields.get('duration_ms')).toEqual({ value: 500, type: 'float' }); + }); + + it('reads top-level fields via _top source', () => { + const dataset: DatasetQuery = { + ...WORKERS_DATASET, + topLevelFields: ['count'], + fields: { + total: { type: 'int', source: ['_top', 'count'] }, + }, + }; + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, + count: 42, + }; + const metric = buildMetric(dataset, row, 'acct', emptyCache()); + expect(metric!.fields.get('total')).toEqual({ value: 42, type: 'int' }); + }); + + it('enriches D1 rows with database_name from cache', () => { + const dataset: DatasetQuery = { + key: 'd1_queries', + measurement: 'cf_d1_queries', + field: 'd1QueriesAdaptiveGroups', + dimensions: ['datetimeMinute', 'databaseId'], + topLevelFields: [], + blocks: { sum: ['readQueries'] }, + tags: [{ source: 'databaseId', as: 'database_id' }], + fields: { read_queries: { type: 'int', source: ['sum', 'readQueries'] } }, + }; + const cache = emptyCache(); + cache.d1Databases.set('db-1', 'my-database'); + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', databaseId: 'db-1' }, + sum: { readQueries: 5 }, + }; + const metric = buildMetric(dataset, row, 'acct', cache); + expect(metric!.tags.get('database_name')).toBe('my-database'); + }); + + it('enriches queue rows with queue_name from cache', () => { + const dataset: DatasetQuery = { + key: 'queue_operations', + measurement: 'cf_queue_operations', + field: 'queueMessageOperationsAdaptiveGroups', + dimensions: ['datetimeMinute', 'queueId'], + topLevelFields: [], + blocks: { sum: ['billableOperations'] }, + tags: [{ source: 'queueId', as: 'queue_id' }], + fields: { billable_operations: { type: 'int', source: ['sum', 'billableOperations'] } }, + }; + const cache = emptyCache(); + cache.queues.set('q-1', 'my-queue'); + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', queueId: 'q-1' }, + sum: { billableOperations: 10 }, + }; + const metric = buildMetric(dataset, row, 'acct', cache); + expect(metric!.tags.get('queue_name')).toBe('my-queue'); + }); + + it('enriches HTTP rows with zone_name, falling back to zone tag', () => { + const dataset: DatasetQuery = { + key: 'http_requests_overview', + measurement: 'cf_http_requests_overview', + field: 'httpRequestsOverviewAdaptiveGroups', + dimensions: ['datetimeMinute', 'zoneTag'], + topLevelFields: [], + blocks: { sum: ['requests'] }, + tags: [{ source: 'zoneTag', as: 'zone_tag' }], + fields: { requests: { type: 'int', source: ['sum', 'requests'] } }, + }; + const cache = emptyCache(); + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', zoneTag: 'zone-xyz' }, + sum: { requests: 1 }, + }; + const metric = buildMetric(dataset, row, 'acct', cache); + expect(metric!.tags.get('zone_name')).toBe('zone-xyz'); + }); + + it('does not apply enrichment for unrecognized dataset fields', () => { + const dataset: DatasetQuery = { + ...WORKERS_DATASET, + field: 'somethingElse', + }; + const row: DatasetRow = { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', scriptName: 'w', status: 's' }, + sum: { requests: 1, errors: 0 }, + }; + const metric = buildMetric(dataset, row, 'acct', emptyCache()); + expect(metric!.tags.has('database_name')).toBe(false); + expect(metric!.tags.has('queue_name')).toBe(false); + expect(metric!.tags.has('zone_name')).toBe(false); + }); +}); diff --git a/apps/cloudflare-metrics/src/emit.ts b/apps/cloudflare-metrics/src/emit.ts new file mode 100644 index 0000000..3024874 --- /dev/null +++ b/apps/cloudflare-metrics/src/emit.ts @@ -0,0 +1,164 @@ +import { Metric } from './metric.js'; +import type { ResourceCache } from './resource-cache.js'; +import type { DatasetQuery, DatasetRow, FieldSpec } from './types.js'; + +// Precomputed per-dataset field entries to avoid Object.entries() per row. +// Keyed by dataset identity — safe because dataset objects are module-level +// constants that live for the lifetime of the isolate. +type FieldEntry = readonly [string, string, string, FieldSpec]; +const fieldEntriesCache = new WeakMap(); + +function getFieldEntries(dataset: DatasetQuery): FieldEntry[] { + let cached = fieldEntriesCache.get(dataset); + if (!cached) { + cached = Object.entries(dataset.fields).map( + ([name, spec]) => [name, spec.source[0], spec.source[1], spec] as const, + ); + fieldEntriesCache.set(dataset, cached); + } + return cached; +} + +// Precomputed per-dataset resource tag enricher. The original code ran a +// `switch(dataset.field)` on every row — bypass that per-row work by +// resolving the correct enricher once per dataset and caching the closure. +type TagEnricher = (metric: Metric, row: DatasetRow, cache: ResourceCache) => void; +const noopEnricher: TagEnricher = () => {}; +const tagEnricherCache = new WeakMap(); + +function getTagEnricher(dataset: DatasetQuery): TagEnricher { + let cached = tagEnricherCache.get(dataset); + if (cached) { + return cached; + } + switch (dataset.field) { + case 'd1AnalyticsAdaptiveGroups': + case 'd1StorageAdaptiveGroups': + case 'd1QueriesAdaptiveGroups': { + cached = (metric, row, cache) => { + const id = row.dimensions?.databaseId; + if (typeof id === 'string' && id !== '') { + const name = cache.d1Databases.get(id); + if (name) { + metric.addTag('database_name', name); + } + } + }; + break; + } + case 'queueMessageOperationsAdaptiveGroups': + case 'queueBacklogAdaptiveGroups': { + cached = (metric, row, cache) => { + const id = row.dimensions?.queueId; + if (typeof id === 'string' && id !== '') { + const name = cache.queues.get(id); + if (name) { + metric.addTag('queue_name', name); + } + } + }; + break; + } + case 'httpRequestsOverviewAdaptiveGroups': { + cached = (metric, row, cache) => { + const tag = row.dimensions?.zoneTag; + if (typeof tag === 'string' && tag !== '') { + metric.addTag('zone_name', cache.zones.get(tag) ?? tag); + } + }; + break; + } + default: { + cached = noopEnricher; + } + } + tagEnricherCache.set(dataset, cached); + return cached; +} + +export function buildMetric( + dataset: DatasetQuery, + row: DatasetRow, + accountTag: string, + resourceCache: ResourceCache, +): Metric | null { + const timestamp = resolveTimestamp(dataset, row); + if (!timestamp) { + return null; + } + + const metric = Metric.create(dataset.measurement); + metric.addTag('account_id', accountTag); + metric.setExportTimestamp(timestamp); + + const dims = row.dimensions; + for (const tag of dataset.tags) { + const raw = dims?.[tag.source]; + const value = normalizeTagValue(raw); + if (value !== undefined) { + metric.addTag(tag.as, value); + } + } + + getTagEnricher(dataset)(metric, row, resourceCache); + + const rowAny = row as unknown as Record; + let hasField = false; + for (const entry of getFieldEntries(dataset)) { + const [fieldName, block, key, spec] = entry; + let raw: number | null | undefined; + if (block === '_top') { + raw = rowAny[key] as number | null | undefined; + } else { + const blockData = rowAny[block] as Record | undefined; + raw = blockData?.[key]; + } + if (raw === null || raw === undefined) { + continue; + } + const value = spec.scale ? raw * spec.scale : raw; + if (spec.type === 'float') { + metric.floatField(fieldName, value); + } else { + metric.intField(fieldName, Math.round(value)); + } + hasField = true; + } + + if (!hasField) { + return null; + } + + return metric; +} + +export function resolveTimestamp(dataset: DatasetQuery, row: DatasetRow): Date | null { + const dimension = dataset.timestampDimension ?? 'datetimeMinute'; + const raw = row.dimensions?.[dimension]; + if (raw === null || raw === undefined) { + return null; + } + const str = String(raw); + // `date` dimension is "YYYY-MM-DD" — treat as UTC midnight. + if (dimension === 'date') { + return new Date(`${str}T00:00:00Z`); + } + const date = new Date(str); + if (Number.isNaN(date.getTime())) { + return null; + } + return date; +} + +export function normalizeTagValue(raw?: unknown): string | undefined { + if (raw === null || raw === undefined) { + return undefined; + } + if (typeof raw === 'string') { + return raw === '' ? undefined : raw; + } + if (typeof raw === 'number' || typeof raw === 'boolean') { + return String(raw); + } + return undefined; +} diff --git a/apps/cloudflare-metrics/src/flush-state.test.ts b/apps/cloudflare-metrics/src/flush-state.test.ts new file mode 100644 index 0000000..432a0ca --- /dev/null +++ b/apps/cloudflare-metrics/src/flush-state.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { + __resetFlushStateForTests, + MAX_PENDING_FLUSH_BYTES, + pendingFlushBuffers, + recordLastFlushStats, + takeLastFlushStats, +} from './flush-state.js'; + +describe('flush-state', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + describe('takeLastFlushStats', () => { + it('returns null when no stats have been recorded', () => { + expect(takeLastFlushStats()).toBeNull(); + }); + + it('returns the recorded stats and clears them', () => { + const stats = { + bytes: 1024, + durationMs: 50, + status: 'ok' as const, + pendingBuffers: 0, + pendingBytes: 0, + }; + recordLastFlushStats(stats); + expect(takeLastFlushStats()).toEqual(stats); + expect(takeLastFlushStats()).toBeNull(); + }); + + it('returns the most recent stats when recorded multiple times', () => { + recordLastFlushStats({ bytes: 100, durationMs: 10, status: 'ok', pendingBuffers: 0, pendingBytes: 0 }); + recordLastFlushStats({ bytes: 200, durationMs: 20, status: 'error', pendingBuffers: 1, pendingBytes: 100 }); + const stats = takeLastFlushStats(); + expect(stats?.bytes).toBe(200); + expect(stats?.status).toBe('error'); + }); + }); + + describe('pendingFlushBuffers', () => { + it('starts empty', () => { + expect(pendingFlushBuffers).toHaveLength(0); + }); + + it('is a mutable array', () => { + pendingFlushBuffers.push('body1', 'body2'); + expect(pendingFlushBuffers).toHaveLength(2); + expect(pendingFlushBuffers[0]).toBe('body1'); + }); + + it('is cleared by reset', () => { + pendingFlushBuffers.push('body'); + __resetFlushStateForTests(); + expect(pendingFlushBuffers).toHaveLength(0); + }); + }); + + describe('MAX_PENDING_FLUSH_BYTES', () => { + it('is 10 MB', () => { + expect(MAX_PENDING_FLUSH_BYTES).toBe(10 * 1024 * 1024); + }); + }); +}); diff --git a/apps/cloudflare-metrics/src/flush-state.ts b/apps/cloudflare-metrics/src/flush-state.ts new file mode 100644 index 0000000..fce3754 --- /dev/null +++ b/apps/cloudflare-metrics/src/flush-state.ts @@ -0,0 +1,28 @@ +export const pendingFlushBuffers: string[] = []; +// Cap retry buffer at 10 MB; eviction drops oldest-first. +export const MAX_PENDING_FLUSH_BYTES = 10 * 1024 * 1024; + +export interface LastFlushStats { + bytes: number; + durationMs: number; + status: 'ok' | 'error'; + pendingBuffers: number; + pendingBytes: number; +} + +let lastFlushStats: LastFlushStats | null = null; + +export function takeLastFlushStats(): LastFlushStats | null { + const stats = lastFlushStats; + lastFlushStats = null; + return stats; +} + +export function recordLastFlushStats(stats: LastFlushStats): void { + lastFlushStats = stats; +} + +export function __resetFlushStateForTests(): void { + pendingFlushBuffers.length = 0; + lastFlushStats = null; +} diff --git a/apps/cloudflare-metrics/src/graphql-builders.ts b/apps/cloudflare-metrics/src/graphql-builders.ts new file mode 100644 index 0000000..b83f407 --- /dev/null +++ b/apps/cloudflare-metrics/src/graphql-builders.ts @@ -0,0 +1,139 @@ +import type { DatasetQuery, GraphQLResponse } from './types.js'; + +export const DEFAULT_DATASET_LIMIT = 9999; + +// Cloudflare GraphQL rejects batched queries above ~50 aliased datasets; +// 25 per chunk leaves headroom for datasets with extra blocks/dimensions. +export const ACCOUNT_BATCH_CHUNK_SIZE = 25; + +export function chunkArray(items: readonly T[], size: number): T[][] { + if (size <= 0) { + return items.length > 0 ? [[...items]] : []; + } + const chunks: T[][] = []; + for (let i = 0; i < items.length; i += size) { + chunks.push(items.slice(i, i + size) as T[]); + } + return chunks; +} + +export function buildDatasetSelection(dataset: DatasetQuery, alias?: string): string { + const dimensionSelection = dataset.dimensions.join(' '); + const blockSelections = (Object.entries(dataset.blocks) as Array<[string, readonly string[] | undefined]>) + .filter(([, fields]) => fields && fields.length > 0) + .map(([block, fields]) => `${block} { ${(fields ?? []).join(' ')} }`) + .join(' '); + const topLevelSelection = (dataset.topLevelFields ?? []).join(' '); + const limit = dataset.limit ?? DEFAULT_DATASET_LIMIT; + const aliasPrefix = alias && alias !== dataset.field ? `${alias}: ` : ''; + return `${aliasPrefix}${dataset.field}(limit: ${limit}, filter: $filter${orderByClause(dataset)}) { + dimensions { ${dimensionSelection} } + ${topLevelSelection} + ${blockSelections} + }`; +} + +const SCHEDULED_INVOCATIONS_SELECTION = `workers_scheduled: workersInvocationsScheduled(limit: ${DEFAULT_DATASET_LIMIT}, filter: $filter) { + scriptName + cron + status + datetime + cpuTimeUs + }`; + +export function buildBatchedAccountQuery( + datasets: readonly DatasetQuery[], + includeScheduledInvocations = false, +): string { + const selections = datasets.map((d) => buildDatasetSelection(d, d.key)); + if (includeScheduledInvocations) { + selections.push(SCHEDULED_INVOCATIONS_SELECTION); + } + return `query CloudflareMetricsAccountBatch($accountTag: String!, $filter: JSON!) { + viewer { + accounts(filter: { accountTag: $accountTag }) { + ${selections.join('\n ')} + } + } +}`; +} + +export function buildBatchedZoneQuery(zoneTags: readonly string[], dataset: DatasetQuery): string { + const safeZoneTags = zoneTags.map((tag) => { + if (!/^[a-zA-Z0-9_-]+$/.test(tag)) { + throw new Error(`Invalid zoneTag for batched query: ${tag}`); + } + return tag; + }); + const selections = safeZoneTags + .map( + (tag, index) => ` z${index}: zones(filter: { zoneTag: "${tag}" }) { + ${buildDatasetSelection(dataset, dataset.field)} + }`, + ) + .join('\n'); + return `query CloudflareMetricsZoneBatch($filter: JSON!) { + viewer { +${selections} + } +}`; +} + +export function buildFilterObject( + dataset: DatasetQuery | null, + range: { start: Date; end: Date }, +): Record { + const filter: Record = dataset?.extraFilter ? { ...dataset.extraFilter } : {}; + filter.datetime_geq = range.start.toISOString(); + filter.datetime_leq = range.end.toISOString(); + return filter; +} + +export function groupErrorsByAlias( + errors: GraphQLResponse['errors'] | null | undefined, +): Record { + const result: Record = {}; + if (!errors) { + return result; + } + for (const error of errors) { + const alias = findAliasInPath(error.path); + if (alias) { + const existing = result[alias]; + result[alias] = existing ? `${existing}; ${error.message}` : error.message; + } + } + return result; +} + +function findAliasInPath(path: readonly (string | number)[] | undefined): string | undefined { + if (!path) { + return undefined; + } + // Typical path shape: + // ['viewer', 'accounts', 0, ''] + // ['viewer', '', 0, ''] (zone batches) + // Return the first path segment after a list index that isn't part of + // the `dimensions` / `sum` / etc. internals. + for (let i = 0; i < path.length; i++) { + const segment = path[i]; + if (typeof segment !== 'string') { + continue; + } + if (segment === 'viewer' || segment === 'accounts' || segment === 'zones') { + continue; + } + return segment; + } + return undefined; +} + +function orderByClause(dataset: DatasetQuery): string { + const timestampDim = dataset.timestampDimension ?? 'datetimeMinute'; + // Grouping implicitly takes place on the dimensions, so orderBy helps + // make sure we get the most recent buckets within the limit. + if (dataset.dimensions.includes(timestampDim)) { + return `, orderBy: [${timestampDim}_ASC]`; + } + return ''; +} diff --git a/apps/cloudflare-metrics/src/graphql-client.test.ts b/apps/cloudflare-metrics/src/graphql-client.test.ts new file mode 100644 index 0000000..e4ca250 --- /dev/null +++ b/apps/cloudflare-metrics/src/graphql-client.test.ts @@ -0,0 +1,423 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ALL_DATASETS, D1_QUERIES, WORKERS_INVOCATIONS } from './datasets.js'; +import { + buildBatchedAccountQuery, + buildBatchedZoneQuery, + buildDatasetSelection, + buildFilterObject, + groupErrorsByAlias, +} from './graphql-builders.js'; +import { CloudflareGraphQLClient, CloudflareGraphQLError } from './graphql-client.js'; +import type { DatasetQuery } from './types.js'; + +describe('buildDatasetSelection', () => { + it('produces an aliased field selection with dimensions and blocks', () => { + const selection = buildDatasetSelection(WORKERS_INVOCATIONS, 'workers_invocations'); + expect(selection).toContain('workers_invocations: workersInvocationsAdaptive(limit: 9999, filter: $filter'); + expect(selection).toContain('dimensions { datetimeMinute scriptName status scriptVersion usageModel }'); + expect(selection).toContain('sum { requests errors'); + expect(selection).toContain('quantiles { cpuTimeP50 cpuTimeP99'); + expect(selection).toContain('orderBy: [datetimeMinute_ASC]'); + }); + + it('omits the alias prefix when the alias matches the field name', () => { + const selection = buildDatasetSelection(WORKERS_INVOCATIONS); + expect(selection.startsWith('workersInvocationsAdaptive(')).toBe(true); + // The GraphQL `filter: $filter` argument also uses `: ` so we can't + // test absence of the colon globally — just assert the alias prefix + // isn't present at the start. + expect(selection).not.toMatch(/^workers_invocations: workersInvocationsAdaptive/); + }); + + it('includes top-level scalar fields between dimensions and blocks', () => { + const dataset: DatasetQuery = { + key: 'x', + measurement: 'cf_x', + field: 'xGroups', + dimensions: ['datetimeMinute'], + topLevelFields: ['count'], + blocks: {}, + tags: [], + fields: { count: { type: 'int', source: ['_top', 'count'] } }, + }; + const selection = buildDatasetSelection(dataset, 'x'); + expect(selection).toMatch(/dimensions \{ datetimeMinute \}\s+count/); + }); + + it('omits orderBy when the timestamp dimension is not selected', () => { + const dataset: DatasetQuery = { ...WORKERS_INVOCATIONS, dimensions: ['scriptName'] }; + const selection = buildDatasetSelection(dataset, 'x'); + expect(selection).not.toContain('orderBy'); + }); +}); + +describe('buildBatchedAccountQuery', () => { + it('wraps multiple aliased selections in a single viewer.accounts query', () => { + const query = buildBatchedAccountQuery([WORKERS_INVOCATIONS, D1_QUERIES]); + expect(query).toContain('$accountTag: String!'); + expect(query).toContain('$filter: JSON!'); + expect(query).toContain('accounts(filter: { accountTag: $accountTag })'); + expect(query).toContain('workers_invocations: workersInvocationsAdaptive('); + expect(query).toContain('d1_queries: d1AnalyticsAdaptiveGroups('); + }); + + it('appends a workersInvocationsScheduled selection when requested', () => { + const query = buildBatchedAccountQuery([WORKERS_INVOCATIONS], true); + expect(query).toContain('workers_scheduled: workersInvocationsScheduled('); + expect(query).toContain('scriptName'); + expect(query).toContain('cpuTimeUs'); + }); + + it('builds a query for every account-scope dataset in the registry', () => { + const accountDatasets = ALL_DATASETS.filter((d) => (d.scope ?? 'account') === 'account'); + const query = buildBatchedAccountQuery(accountDatasets, true); + for (const dataset of accountDatasets) { + expect(query).toContain(`${dataset.key}: ${dataset.field}(`); + } + }); +}); + +describe('buildBatchedZoneQuery', () => { + const dataset: DatasetQuery = { + key: 'zone_detail', + measurement: 'cf_zone_detail', + field: 'httpRequestsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute'], + blocks: { sum: ['edgeResponseBytes'] }, + tags: [], + fields: { bytes: { type: 'int', source: ['sum', 'edgeResponseBytes'] } }, + }; + + it('aliases each zone with an inlined zoneTag literal', () => { + const query = buildBatchedZoneQuery(['zone-a', 'zone-b'], dataset); + expect(query).toContain('z0: zones(filter: { zoneTag: "zone-a" })'); + expect(query).toContain('z1: zones(filter: { zoneTag: "zone-b" })'); + expect(query).toContain('httpRequestsAdaptiveGroups('); + }); + + it('rejects zone tags with unsafe characters', () => { + expect(() => buildBatchedZoneQuery(['zone-a"; evil'], dataset)).toThrow(/Invalid zoneTag/); + }); +}); + +describe('buildFilterObject', () => { + const range = { + start: new Date('2026-04-10T12:00:00Z'), + end: new Date('2026-04-10T12:10:00Z'), + }; + + it('uses datetime_geq / datetime_leq by default', () => { + expect(buildFilterObject(WORKERS_INVOCATIONS, range)).toEqual({ + datetime_geq: '2026-04-10T12:00:00.000Z', + datetime_leq: '2026-04-10T12:10:00.000Z', + }); + }); + + it('falls back to datetime filters when no dataset is supplied', () => { + expect(buildFilterObject(null, range)).toEqual({ + datetime_geq: '2026-04-10T12:00:00.000Z', + datetime_leq: '2026-04-10T12:10:00.000Z', + }); + }); +}); + +describe('groupErrorsByAlias', () => { + it('maps GraphQL error paths to aliased field names', () => { + const errors = [ + { message: 'boom', path: ['viewer', 'accounts', 0, 'workers_invocations'] }, + { message: 'bang', path: ['viewer', 'accounts', 0, 'd1_queries'] }, + ]; + expect(groupErrorsByAlias(errors)).toEqual({ + workers_invocations: 'boom', + d1_queries: 'bang', + }); + }); + + it('returns an empty map when errors is null or undefined', () => { + expect(groupErrorsByAlias(null)).toEqual({}); + // eslint-disable-next-line unicorn/no-useless-undefined + expect(groupErrorsByAlias(undefined)).toEqual({}); + }); +}); + +describe('CloudflareGraphQLClient', () => { + it('sends the bearer token and parses batched dataset rows', async () => { + const fetchMock = vi.fn(() => + Promise.resolve( + new Response( + JSON.stringify({ + data: { + viewer: { + accounts: [ + { + workers_invocations: [ + { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z', scriptName: 'a' }, + sum: { requests: 10 }, + }, + ], + }, + ], + }, + }, + errors: null, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ), + ), + ); + + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + const result = await client.fetchAccountBatch('acct', [WORKERS_INVOCATIONS], { + start: new Date('2026-04-10T12:00:00Z'), + end: new Date('2026-04-10T12:10:00Z'), + }); + + expect(result.rows.workers_invocations).toHaveLength(1); + expect(result.rows.workers_invocations?.[0].dimensions.scriptName).toBe('a'); + const call = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; + const init = call[1]; + expect((init.headers as Record).Authorization).toBe('Bearer tok'); + const body = JSON.parse(init.body as string); + expect(body.variables.accountTag).toBe('acct'); + expect(body.query).toContain('workers_invocations: workersInvocationsAdaptive'); + }); + + it('throws a CloudflareGraphQLError on HTTP failure', async () => { + const fetchMock = vi.fn(() => Promise.resolve(new Response('boom', { status: 500 }))); + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + await expect( + client.fetchAccountBatch('acct', [WORKERS_INVOCATIONS], { start: new Date(0), end: new Date(1) }), + ).rejects.toBeInstanceOf(CloudflareGraphQLError); + }); + + it('surfaces per-field errors from a partial GraphQL response', async () => { + const fetchMock = vi.fn(() => + Promise.resolve( + new Response( + JSON.stringify({ + data: { + viewer: { + accounts: [ + { + workers_invocations: [ + { + dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, + sum: { requests: 1 }, + }, + ], + d1_queries: null, + }, + ], + }, + }, + errors: [{ message: 'no access', path: ['viewer', 'accounts', 0, 'd1_queries'] }], + }), + { status: 200 }, + ), + ), + ); + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + const result = await client.fetchAccountBatch('acct', [WORKERS_INVOCATIONS, D1_QUERIES], { + start: new Date(0), + end: new Date(1), + }); + expect(result.rows.workers_invocations).toHaveLength(1); + expect(result.errors.d1_queries).toBe('no access'); + }); + + it('splits account batches into chunks under the graphql node limit', async () => { + // Build a fake dataset list that's large enough to span multiple chunks. + const datasets = Array.from({ length: 60 }, (_, i) => ({ + ...WORKERS_INVOCATIONS, + key: `ds_${i}`, + measurement: `cf_ds_${i}`, + })); + const fetchMock = vi.fn((_url: string, init: RequestInit) => { + const body = JSON.parse(init.body as string); + // Return the aliased fields that this chunk actually asked for. + const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); + const account: Record = {}; + for (const alias of aliases) { + account[alias] = []; + } + return Promise.resolve( + new Response(JSON.stringify({ data: { viewer: { accounts: [account] } }, errors: null }), { status: 200 }), + ); + }); + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + const result = await client.fetchAccountBatch('acct', datasets, { + start: new Date('2026-04-10T12:00:00Z'), + end: new Date('2026-04-10T12:10:00Z'), + }); + // Each chunk is its own HTTP subrequest. + expect(fetchMock.mock.calls.length).toBeGreaterThan(1); + // All datasets get a row entry, even if empty — nothing is dropped across chunks. + expect(Object.keys(result.rows)).toHaveLength(60); + expect(Object.keys(result.errors)).toHaveLength(0); + }); + + it('only includes the scheduled invocations field on the first chunk', async () => { + // 60 datasets span 3 chunks at the default chunk size of 25. + const datasets = Array.from({ length: 60 }, (_, i) => ({ + ...WORKERS_INVOCATIONS, + key: `ds_${i}`, + measurement: `cf_ds_${i}`, + })); + const fetchMock = vi.fn((_url: string, init: RequestInit) => { + const body = JSON.parse(init.body as string); + const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); + const account: Record = {}; + for (const alias of aliases) { + account[alias] = []; + } + // When this chunk includes the scheduled feed, echo a single row back + // so we can verify the collector only sees it once. + if (body.query.includes('workers_scheduled: workersInvocationsScheduled')) { + (account as Record).workers_scheduled = [ + { + scriptName: 's', + cron: '* * * * *', + status: 'success', + datetime: '2026-04-10T12:00:00Z', + cpuTimeUs: 100, + }, + ]; + } + return Promise.resolve( + new Response(JSON.stringify({ data: { viewer: { accounts: [account] } }, errors: null }), { status: 200 }), + ); + }); + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + const result = await client.fetchAccountBatch( + 'acct', + datasets, + { + start: new Date('2026-04-10T12:00:00Z'), + end: new Date('2026-04-10T12:10:00Z'), + }, + { includeScheduledInvocations: true }, + ); + + // Exactly one chunk must include the scheduled-invocations selection; + // the others should not ask for it at all. + const scheduledCount = fetchMock.mock.calls.filter((call) => { + const body = JSON.parse((call[1] as RequestInit).body as string); + return (body.query as string).includes('workers_scheduled: workersInvocationsScheduled'); + }).length; + expect(scheduledCount).toBe(1); + // And the merged result should carry exactly one scheduled row. + expect(result.scheduledInvocations).toHaveLength(1); + }); + + it('batches multiple zones into a single request', async () => { + const fetchMock = vi.fn(() => + Promise.resolve( + new Response( + JSON.stringify({ + data: { + viewer: { + z0: [ + { + httpRequestsAdaptiveGroups: [{ dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, count: 10 }], + }, + ], + z1: [ + { + httpRequestsAdaptiveGroups: [{ dimensions: { datetimeMinute: '2026-04-10T12:00:00Z' }, count: 20 }], + }, + ], + }, + }, + errors: null, + }), + { status: 200 }, + ), + ), + ); + const zoneScoped: DatasetQuery = { + key: 'zone_detail', + measurement: 'cf_zone_detail', + field: 'httpRequestsAdaptiveGroups', + scope: 'zone', + dimensions: ['datetimeMinute'], + topLevelFields: ['count'], + blocks: {}, + tags: [], + fields: { requests: { type: 'int', source: ['_top', 'count'] } }, + }; + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + const result = await client.fetchZoneBatch(['zone-a', 'zone-b'], zoneScoped, { + start: new Date(0), + end: new Date(1), + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(result.rows['zone-a']).toHaveLength(1); + expect(result.rows['zone-b']).toHaveLength(1); + expect(result.rows['zone-a']?.[0].count).toBe(10); + expect(result.rows['zone-b']?.[0].count).toBe(20); + }); + + it('counts graphql requests and error responses across multiple chunks', async () => { + const datasets = Array.from({ length: 30 }, (_, i) => ({ + ...WORKERS_INVOCATIONS, + key: `ds_${i}`, + measurement: `cf_ds_${i}`, + })); + let calls = 0; + const fetchMock = vi.fn((_url: string, init: RequestInit) => { + calls++; + const body = JSON.parse(init.body as string); + const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); + const account: Record = {}; + for (const alias of aliases) { + account[alias] = []; + } + // Return an error payload on the second chunk so `errorResponseCount` + // has something to count. + const errors = + calls === 2 ? [{ message: 'transient', path: ['viewer', 'accounts', 0, aliases[0] ?? 'ds_0'] }] : null; + return Promise.resolve( + new Response(JSON.stringify({ data: { viewer: { accounts: [account] } }, errors }), { status: 200 }), + ); + }); + const client = new CloudflareGraphQLClient( + 'tok', + 'https://example.com/graphql', + fetchMock as unknown as typeof fetch, + ); + await client.fetchAccountBatch('acct', datasets, { + start: new Date('2026-04-10T12:00:00Z'), + end: new Date('2026-04-10T12:10:00Z'), + }); + + expect(client.requestCount).toBe(fetchMock.mock.calls.length); + expect(client.requestCount).toBeGreaterThanOrEqual(2); + expect(client.errorResponseCount).toBe(1); + }); +}); diff --git a/apps/cloudflare-metrics/src/graphql-client.ts b/apps/cloudflare-metrics/src/graphql-client.ts new file mode 100644 index 0000000..221c870 --- /dev/null +++ b/apps/cloudflare-metrics/src/graphql-client.ts @@ -0,0 +1,285 @@ +import { + ACCOUNT_BATCH_CHUNK_SIZE, + buildBatchedAccountQuery, + buildBatchedZoneQuery, + buildFilterObject, + chunkArray, + groupErrorsByAlias, +} from './graphql-builders.js'; +import type { AccountQueryResult, DatasetQuery, DatasetRow, GraphQLResponse } from './types.js'; + +const CLOUDFLARE_GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql'; + +export class CloudflareGraphQLError extends Error { + constructor( + message: string, + public readonly statusCode: number, + public readonly responseBody?: string, + ) { + super(message); + this.name = 'CloudflareGraphQLError'; + } +} + +export interface ScheduledWorkerInvocation { + scriptName: string; + cron: string; + status: string; + datetime: string; + cpuTimeUs: number; +} + +export interface BatchedDatasetResult { + rows: Record; + errors: Record; + scheduledInvocations?: ScheduledWorkerInvocation[]; +} + +export interface BatchedZoneDatasetResult { + rows: Record; + errors: Record; +} + +export interface ICloudflareGraphQLClient { + fetchAccountBatch( + accountTag: string, + datasets: readonly DatasetQuery[], + range: { start: Date; end: Date }, + options?: { includeScheduledInvocations?: boolean }, + ): Promise; + fetchZoneBatch( + zoneTags: readonly string[], + dataset: DatasetQuery, + range: { start: Date; end: Date }, + ): Promise; +} + +export class CloudflareGraphQLClient implements ICloudflareGraphQLClient { + private _requestCount = 0; + private _errorResponseCount = 0; + private _retryCount = 0; + private _retrySuccessCount = 0; + + constructor( + private readonly apiToken: string, + private readonly endpoint: string = CLOUDFLARE_GRAPHQL_ENDPOINT, + private readonly fetchImpl?: typeof fetch, + ) {} + + get requestCount(): number { + return this._requestCount; + } + + get errorResponseCount(): number { + return this._errorResponseCount; + } + + get retryCount(): number { + return this._retryCount; + } + + get retrySuccessCount(): number { + return this._retrySuccessCount; + } + + async fetchAccountBatch( + accountTag: string, + datasets: readonly DatasetQuery[], + range: { start: Date; end: Date }, + options: { includeScheduledInvocations?: boolean } = {}, + ): Promise { + // Chunk under the ~50-dataset ceiling enforced by Cloudflare's GraphQL endpoint. + const chunks = chunkArray(datasets, ACCOUNT_BATCH_CHUNK_SIZE); + if (chunks.length === 0 && options.includeScheduledInvocations) { + chunks.push([]); + } + const result: BatchedDatasetResult = { rows: {}, errors: {} }; + + for (const [i, chunk] of chunks.entries()) { + const includeScheduled = i === 0 && (options.includeScheduledInvocations ?? false); + const chunkResult = await this.fetchAccountBatchChunk(accountTag, chunk, range, includeScheduled); + Object.assign(result.rows, chunkResult.rows); + Object.assign(result.errors, chunkResult.errors); + if (includeScheduled) { + if (chunkResult.errors.workers_scheduled) { + result.errors.workers_scheduled = chunkResult.errors.workers_scheduled; + } else { + result.scheduledInvocations = chunkResult.scheduledInvocations ?? []; + } + } + } + + return result; + } + + private async fetchAccountBatchChunk( + accountTag: string, + datasets: readonly DatasetQuery[], + range: { start: Date; end: Date }, + includeScheduledInvocations: boolean, + attempt = 1, + ): Promise { + const query = buildBatchedAccountQuery(datasets, includeScheduledInvocations); + const variables = { + accountTag, + filter: buildFilterObject(datasets[0] ?? null, range), + }; + const { data, errors } = await this.executeAllowPartial(query, variables); + const result: BatchedDatasetResult = { rows: {}, errors: {} }; + + const errorsByAlias = groupErrorsByAlias(errors); + const account = data?.viewer.accounts[0]; + for (const dataset of datasets) { + if (errorsByAlias[dataset.key]) { + result.errors[dataset.key] = errorsByAlias[dataset.key]; + continue; + } + const rows = account?.[dataset.key] as unknown as DatasetRow[] | undefined; + result.rows[dataset.key] = rows ?? []; + } + + if (includeScheduledInvocations) { + if (errorsByAlias.workers_scheduled) { + result.errors.workers_scheduled = errorsByAlias.workers_scheduled; + } else { + result.scheduledInvocations = (account?.workers_scheduled as unknown as ScheduledWorkerInvocation[]) ?? []; + } + } + + // If the query returned no data and no aliased errors, surface a single + // top-level error so callers can report it uniformly. + if (!account && errors && errors.length > 0 && Object.keys(result.errors).length === 0) { + const message = errors.map((e) => e.message).join('; '); + for (const dataset of datasets) { + result.errors[dataset.key] = message; + } + if (includeScheduledInvocations) { + result.errors.workers_scheduled = message; + } + } + + // Retry once when all fields errored — likely a transient Cloudflare + // analytics backend issue that resolves on the next attempt. + const totalFields = datasets.length + (includeScheduledInvocations ? 1 : 0); + if (attempt < 2 && totalFields > 0 && Object.keys(result.errors).length >= totalFields) { + this._retryCount++; + console.warn(`[graphql] all ${totalFields} fields errored, retrying chunk (attempt ${attempt + 1})`); + // Undo the error response count from this attempt — the retry will + // re-increment if it also fails, keeping the counter accurate. + this._errorResponseCount--; + await sleep(250); + const retryResult = await this.fetchAccountBatchChunk( + accountTag, + datasets, + range, + includeScheduledInvocations, + attempt + 1, + ); + if (Object.keys(retryResult.errors).length < totalFields) { + this._retrySuccessCount++; + } + return retryResult; + } + + return result; + } + + async fetchZoneBatch( + zoneTags: readonly string[], + dataset: DatasetQuery, + range: { start: Date; end: Date }, + attempt = 1, + ): Promise { + if (zoneTags.length === 0) { + return { rows: {}, errors: {} }; + } + const query = buildBatchedZoneQuery(zoneTags, dataset); + const variables = { + filter: buildFilterObject(dataset, range), + }; + const { data, errors } = await this.executeAllowPartial<{ + viewer: Record>>; + }>(query, variables); + + const result: BatchedZoneDatasetResult = { rows: {}, errors: {} }; + const errorsByAlias = groupErrorsByAlias(errors); + const viewer = data?.viewer ?? {}; + for (const [index, zoneTag] of zoneTags.entries()) { + const alias = `z${index}`; + if (errorsByAlias[alias]) { + result.errors[zoneTag] = errorsByAlias[alias]; + continue; + } + const zoneBlock = viewer[alias]?.[0]; + const rows = zoneBlock?.[dataset.field] ?? []; + result.rows[zoneTag] = rows; + } + if (errors && errors.length > 0 && Object.keys(result.errors).length === 0) { + const message = errors.map((e) => e.message).join('; '); + for (const zoneTag of zoneTags) { + result.errors[zoneTag] = message; + } + } + + // Retry once when all zones errored. + if (attempt < 2 && zoneTags.length > 0 && Object.keys(result.errors).length >= zoneTags.length) { + this._retryCount++; + console.warn( + `[graphql] all ${zoneTags.length} zones errored for ${dataset.key}, retrying (attempt ${attempt + 1})`, + ); + this._errorResponseCount--; + await sleep(250); + const retryResult = await this.fetchZoneBatch(zoneTags, dataset, range, attempt + 1); + if (Object.keys(retryResult.errors).length < zoneTags.length) { + this._retrySuccessCount++; + } + return retryResult; + } + + return result; + } + + async executeAllowPartial(query: string, variables: Record): Promise> { + const doFetch = this.fetchImpl ?? globalThis.fetch; + if (typeof doFetch !== 'function') { + throw new TypeError(`fetch is not a function (typeof=${typeof doFetch})`); + } + this._requestCount++; + const response = await doFetch(this.endpoint, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query, variables }), + }); + if (!response.ok) { + const body = await safeReadText(response); + throw new CloudflareGraphQLError(`Cloudflare GraphQL HTTP error (${response.status})`, response.status, body); + } + const parsed = (await response.json()) as GraphQLResponse; + if (parsed.errors && parsed.errors.length > 0) { + this._errorResponseCount++; + const uniqueMessages = [...new Set(parsed.errors.map((e) => e.message))]; + const paths = parsed.errors.slice(0, 5).map((e) => e.path?.join('.') ?? 'unknown'); + console.warn( + `[graphql] response contained ${parsed.errors.length} errors:`, + uniqueMessages.join('; '), + `(paths: ${paths.join(', ')}${parsed.errors.length > 5 ? ', ...' : ''})`, + ); + } + return parsed; + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function safeReadText(response: Response): Promise { + try { + return await response.text(); + } catch { + return undefined; + } +} diff --git a/apps/cloudflare-metrics/src/handler.test.ts b/apps/cloudflare-metrics/src/handler.test.ts new file mode 100644 index 0000000..ea1ebd8 --- /dev/null +++ b/apps/cloudflare-metrics/src/handler.test.ts @@ -0,0 +1,98 @@ +import { createExecutionContext, createScheduledController, env, SELF, waitOnExecutionContext } from 'cloudflare:test'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { __resetFlushStateForTests } from './flush-state.js'; +import worker from './index.js'; + +describe('HTTP handler', () => { + it('returns 200 for /health', async () => { + const response = await SELF.fetch('https://example.com/health'); + expect(response.status).toBe(200); + const body = (await response.json()) as { status: string }; + expect(body.status).toBe('ok'); + }); + + it('returns 404 for unknown routes', async () => { + const response = await SELF.fetch('https://example.com/nope'); + expect(response.status).toBe(404); + }); + + it('returns 404 for /collect — the manual trigger endpoint has been removed', async () => { + const response = await SELF.fetch('https://example.com/collect'); + expect(response.status).toBe(404); + }); +}); + +describe('scheduled handler', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + it('emits cron_error{reason=missing_config} when API token or account ID are missing', async () => { + // Collect metric flush bodies seen by the victoria-metrics POST so we + // can assert on the cron_error line without touching internal state. + const originalFetch = globalThis.fetch; + const flushBodies: string[] = []; + globalThis.fetch = ((url: RequestInfo | URL, init?: RequestInit) => { + const target = typeof url === 'string' ? url : url.toString(); + if (target.includes('/write')) { + flushBodies.push(init?.body as string); + return Promise.resolve(new Response('', { status: 204 })); + } + return Promise.resolve(new Response('', { status: 200 })); + }) as typeof fetch; + + const controller = createScheduledController(); + const ctx = createExecutionContext(); + try { + // env has no CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID bindings + // in the test config, so this hits the early-return missing-config + // branch. + await worker.scheduled?.(controller, env as unknown as Env, ctx); + await waitOnExecutionContext(ctx); + } finally { + globalThis.fetch = originalFetch; + } + + const combined = flushBodies.join('\n'); + expect(combined).toContain('cloudflare_metrics_cron_error'); + expect(combined).toContain('reason=missing_config'); + }); + + it('pushes flush self-telemetry to victoria-metrics on the next tick', async () => { + // First tick: a successful flush populates lastFlushStats. + // Second tick: takeLastFlushStats() drains it and emits + // cloudflare_metrics_flush_* lines to the second tick's flush body. + const originalFetch = globalThis.fetch; + const flushBodies: string[] = []; + globalThis.fetch = ((url: RequestInfo | URL, init?: RequestInit) => { + const target = typeof url === 'string' ? url : url.toString(); + if (target.includes('/write')) { + flushBodies.push(init?.body as string); + return Promise.resolve(new Response('', { status: 204 })); + } + return Promise.resolve(new Response('', { status: 200 })); + }) as typeof fetch; + + try { + const controller = createScheduledController(); + // Tick 1 — missing-config path still triggers a flush. + const ctx1 = createExecutionContext(); + await worker.scheduled?.(controller, env as unknown as Env, ctx1); + await waitOnExecutionContext(ctx1); + + // Tick 2 — should emit the flush self-telemetry from tick 1's flush. + const ctx2 = createExecutionContext(); + await worker.scheduled?.(controller, env as unknown as Env, ctx2); + await waitOnExecutionContext(ctx2); + } finally { + globalThis.fetch = originalFetch; + } + + expect(flushBodies.length).toBeGreaterThanOrEqual(2); + // The second body should contain flush-* lines driven by lastFlushStats + // captured during the first flush. + const secondBody = flushBodies[1] ?? ''; + expect(secondBody).toContain('cloudflare_metrics_flush'); + expect(secondBody).toMatch(/cloudflare_metrics_flush[^\s]*status=ok/); + }); +}); diff --git a/apps/cloudflare-metrics/src/handlers/http.ts b/apps/cloudflare-metrics/src/handlers/http.ts new file mode 100644 index 0000000..2da12d8 --- /dev/null +++ b/apps/cloudflare-metrics/src/handlers/http.ts @@ -0,0 +1,88 @@ +import { DeferredRepository } from '../deferred.js'; +import { HeaderMetricsProvider, InfluxMetricsProvider } from '../metric-providers.js'; +import { Metric } from '../metric.js'; +import { CloudflareMetricsRepository } from '../metrics.js'; + +export async function handleFetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const deferredRepository = new DeferredRepository(ctx); + const headerProvider = new HeaderMetricsProvider(); + const influxProvider = new InfluxMetricsProvider(env.VMETRICS_API_TOKEN ?? '', env.ENVIRONMENT ?? ''); + deferredRepository.defer(() => influxProvider.flush()); + const metrics = new CloudflareMetricsRepository( + 'cloudflare_metrics', + request, + [influxProvider, headerProvider], + env.ENVIRONMENT ?? '', + ); + + try { + const response = await metrics.monitorAsyncFunction({ name: 'handle_request' }, () => routeRequest(request))(); + + const url = new URL(request.url); + metrics.push( + Metric.create('http_response') + .addTag('method', request.method) + .addTag('path', url.pathname) + .addTag('status', String(response.status)) + .intField('count', 1), + ); + + response.headers.set('Server-Timing', headerProvider.getTimingHeader()); + deferredRepository.runDeferred(); + return response; + } catch (error) { + console.error(error); + metrics.push( + Metric.create('http_response') + .addTag('method', request.method) + .addTag('path', new URL(request.url).pathname) + .addTag('status', '500') + .intField('count', 1), + ); + deferredRepository.runDeferred(); + return errorResponse('Internal Server Error', 500); + } +} + +function routeRequest(request: Request): Promise { + const url = new URL(request.url); + + if (request.method === 'OPTIONS') { + return Promise.resolve( + new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + }, + }), + ); + } + + switch (url.pathname) { + case '/health': { + return Promise.resolve(jsonResponse({ status: 'ok' })); + } + + default: { + return Promise.resolve(errorResponse('Not Found', 404)); + } + } +} + +const DEFAULT_HEADERS: Record = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', +}; + +function jsonResponse(data: unknown, status = 200, extraHeaders: Record = {}) { + return new Response(JSON.stringify(data), { + status, + headers: { ...DEFAULT_HEADERS, ...extraHeaders }, + }); +} + +function errorResponse(error: string, status: number) { + return jsonResponse({ error }, status); +} diff --git a/apps/cloudflare-metrics/src/handlers/scheduled.ts b/apps/cloudflare-metrics/src/handlers/scheduled.ts new file mode 100644 index 0000000..bd38582 --- /dev/null +++ b/apps/cloudflare-metrics/src/handlers/scheduled.ts @@ -0,0 +1,166 @@ +import { CloudflareRestClient } from '../cloudflare-api.js'; +import { CloudflareMetricsCollector } from '../collector.js'; +import { ALL_DATASETS } from '../datasets.js'; +import { takeLastFlushStats } from '../flush-state.js'; +import { CloudflareGraphQLClient } from '../graphql-client.js'; +import { InfluxMetricsProvider } from '../metric-providers.js'; +import { Metric } from '../metric.js'; +import { CloudflareMetricsRepository } from '../metrics.js'; + +// Lazy-init on first handler call because Date.now() at module scope +// returns a frozen value in Workers (deploy time, not wall clock). +let isolateStartedAt: number | null = null; + +// Track the end timestamp of the last successful collection so we can +// backfill the exact gap after crashes or missed ticks. Resets on isolate +// restart, in which case we fall back to a wide backfill window. +let lastSuccessfulEndMs: number | null = null; + +export async function handleScheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise { + isolateStartedAt ??= Date.now(); + // Cloudflare occasionally drops or delays cron triggers. When it recovers + // from a gap it fires the missed triggers in a burst (~10 at once in + // practice) all at the same wall-clock time. If we used Date.now() for + // the query window, every catch-up invocation would query the same + // "now - lag" range and the originally-scheduled minutes would be lost. + // controller.scheduledTime is the time the cron was originally scheduled + // for, so each catch-up tick covers its own intended window. + const scheduledMs = controller.scheduledTime; + const influxProvider = new InfluxMetricsProvider(env.VMETRICS_API_TOKEN ?? '', env.ENVIRONMENT ?? ''); + const request = new Request('https://localhost/cron'); + const metrics = new CloudflareMetricsRepository( + 'cloudflare_metrics', + request, + [influxProvider], + env.ENVIRONMENT ?? '', + ); + + // Emit telemetry from the previous tick's flush (can't observe the flush + // we're about to make from inside it). `takeLastFlushStats` clears after + // read so a tick without a prior flush just skips these metrics. + emitLastFlushStats(metrics); + + if (!env.CLOUDFLARE_API_TOKEN || !env.CLOUDFLARE_ACCOUNT_ID) { + console.error('[cron] Missing CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID — skipping collection'); + metrics.push(Metric.create('cron_error').addTag('reason', 'missing_config').intField('count', 1)); + ctx.waitUntil(influxProvider.flush()); + return; + } + + const isolateAgeSec = Math.round((Date.now() - isolateStartedAt) / 1000); + const isColdStart = isolateAgeSec < 120; + + const graphqlClient = new CloudflareGraphQLClient(env.CLOUDFLARE_API_TOKEN); + try { + const results = await metrics.monitorAsyncFunction({ name: 'cron_collect' }, () => + runCollection(env, metrics, graphqlClient, isColdStart, lastSuccessfulEndMs, scheduledMs), + )(); + // Record the lagged end of the window we just queried so the next tick + // knows where to pick up if there was a gap. Use scheduledMs (not wall + // clock) so catch-up invocations advance the pointer in order. + const prevEnd = lastSuccessfulEndMs; + const thisEnd = scheduledMs - DEFAULT_LAG_MS; + if (lastSuccessfulEndMs === null || thisEnd > lastSuccessfulEndMs) { + lastSuccessfulEndMs = thisEnd; + } + + const totalPoints = results.reduce((acc, r) => acc + r.points, 0); + const totalErrors = results.filter((r) => r.error).length; + const wasBackfill = isColdStart || prevEnd === null; + metrics.push( + Metric.create('cron_summary') + .intField('datasets', results.length) + .intField('points', totalPoints) + .intField('errors', totalErrors) + .intField('cold_start', isColdStart ? 1 : 0), + ); + console.log( + `[cron] collected ${totalPoints} points across ${results.length} datasets (${totalErrors} errors${wasBackfill ? ', backfill' : ''})`, + ); + } catch (error) { + console.error('[cron] Collection failed:', error); + metrics.push( + Metric.create('cron_error') + .addTag('reason', error instanceof Error ? error.name : 'unknown') + .intField('count', 1), + ); + } + + for (const m of [ + Metric.create('graphql_client') + .intField('requests', graphqlClient.requestCount) + .intField('error_responses', graphqlClient.errorResponseCount) + .intField('retries', graphqlClient.retryCount) + .intField('retry_successes', graphqlClient.retrySuccessCount), + Metric.create('isolate').intField('age_seconds', isolateAgeSec), + ]) { + metrics.push(m); + } + + ctx.waitUntil(influxProvider.flush()); +} + +function emitLastFlushStats(metrics: CloudflareMetricsRepository): void { + const stats = takeLastFlushStats(); + if (!stats) { + return; + } + metrics.push( + Metric.create('flush') + .addTag('status', stats.status) + .intField('bytes', stats.bytes) + .intField('duration_ms', Math.round(stats.durationMs)) + .intField('pending_buffers', stats.pendingBuffers) + .intField('pending_bytes', stats.pendingBytes) + .intField('errors', stats.status === 'error' ? 1 : 0) + .intField('count', 1), + ); +} + +// Re-export for use in window calculation. Must stay in sync with the +// collector's default — but avoids a circular import. +const DEFAULT_LAG_MS = 5 * 60 * 1000; +const DEFAULT_WINDOW_MS = 5 * 60 * 1000; +const MAX_BACKFILL_MS = 30 * 60 * 1000; // 30-minute cap on recovery backfill + +function computeWindowMs(isColdStart: boolean, lastSuccessfulEndMs: number | null, scheduledMs: number): number { + if (lastSuccessfulEndMs !== null) { + // We know exactly when we last queried — extend the window to cover the + // gap. The collector's end = scheduled - lag, so the gap is + // (scheduled - lag) - lastEnd. Using scheduled time (not wall clock) + // so catch-up invocations compute the gap correctly. + const gapMs = scheduledMs - DEFAULT_LAG_MS - lastSuccessfulEndMs; + if (gapMs > DEFAULT_WINDOW_MS) { + return Math.min(gapMs, MAX_BACKFILL_MS); + } + return DEFAULT_WINDOW_MS; + } + // Fresh isolate with no prior state — use a shorter backfill window + // since cold-start ticks already do extra work resolving resource caches. + if (isColdStart) { + return 15 * 60 * 1000; + } + return DEFAULT_WINDOW_MS; +} + +async function runCollection( + env: Env, + metrics: CloudflareMetricsRepository, + graphqlClient: CloudflareGraphQLClient, + isColdStart: boolean, + prevEndMs: number | null, + scheduledMs: number, +) { + const restClient = new CloudflareRestClient(env.CLOUDFLARE_API_TOKEN ?? ''); + const windowMs = computeWindowMs(isColdStart, prevEndMs, scheduledMs); + // Anchor the collector's "now" to the cron's scheduled time, not wall + // clock. This keeps catch-up invocations (fired late by Cloudflare) + // querying their originally-intended window instead of whatever the + // wall clock says at recovery time. + const collector = new CloudflareMetricsCollector(graphqlClient, env.CLOUDFLARE_ACCOUNT_ID, metrics, { + restClient, + windowMs, + now: () => new Date(scheduledMs), + }); + return collector.collectAll(ALL_DATASETS); +} diff --git a/apps/cloudflare-metrics/src/index.ts b/apps/cloudflare-metrics/src/index.ts new file mode 100644 index 0000000..54bb75d --- /dev/null +++ b/apps/cloudflare-metrics/src/index.ts @@ -0,0 +1,12 @@ +import { handleFetch } from './handlers/http.js'; +import { handleScheduled } from './handlers/scheduled.js'; + +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + return handleFetch(request, env, ctx); + }, + + scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise { + return handleScheduled(controller, env, ctx); + }, +}; diff --git a/apps/cloudflare-metrics/src/integration.test.ts b/apps/cloudflare-metrics/src/integration.test.ts new file mode 100644 index 0000000..0017b52 --- /dev/null +++ b/apps/cloudflare-metrics/src/integration.test.ts @@ -0,0 +1,150 @@ +import { env } from 'cloudflare:test'; +import { describe, expect, it } from 'vitest'; +import { CloudflareMetricsCollector } from './collector.js'; +import { ALL_DATASETS } from './datasets.js'; +import { CloudflareGraphQLClient } from './graphql-client.js'; +import type { IMetricsProviderRepository } from './metric-providers.js'; +import { Metric } from './metric.js'; +import { CloudflareMetricsRepository } from './metrics.js'; +import type { DatasetRow } from './types.js'; + +/** + * Integration tests: these hit the real Cloudflare GraphQL Analytics API. + * + * They are gated on `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` being + * set (see `vitest.integration.config.ts` — the pool passes the variables + * through as miniflare bindings). Run with: + * + * CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... \ + * pnpm --filter @immich-services/cloudflare-metrics run test:integration + */ + +const hasCredentials = Boolean(env.CLOUDFLARE_API_TOKEN && env.CLOUDFLARE_ACCOUNT_ID); + +class RecordingProvider implements IMetricsProviderRepository { + readonly metrics: Metric[] = []; + pushMetric(metric: Metric) { + this.metrics.push(metric); + } + flush() { + /* noop */ + } +} + +function buildClient() { + return new CloudflareGraphQLClient(env.CLOUDFLARE_API_TOKEN ?? ''); +} + +function buildMetricsRepo(provider: IMetricsProviderRepository) { + return new CloudflareMetricsRepository( + 'cloudflare_metrics', + new Request('https://localhost/integration'), + [provider], + '', + ); +} + +function wideRange() { + // Use a 1h lookback window ending 10m ago so we land in a fully + // populated bucket without pulling an unreasonable amount of data. + const end = new Date(Date.now() - 10 * 60 * 1000); + const start = new Date(end.getTime() - 60 * 60 * 1000); + return { start, end }; +} + +const INTEGRATION_TIMEOUT_MS = 60_000; + +describe.skipIf(!hasCredentials)('Cloudflare GraphQL integration', () => { + const client = buildClient(); + + // Account-scoped datasets are fetched in a single batched request, + // then iterated individually so we can assert on the shape per dataset. + // Zone-scoped datasets are tested separately below. + const accountDatasets = ALL_DATASETS.filter((d) => (d.scope ?? 'account') === 'account'); + + it( + 'fetches every account-scope dataset in a single batched request', + async () => { + const range = wideRange(); + + const result = await client.fetchAccountBatch(env.CLOUDFLARE_ACCOUNT_ID ?? '', accountDatasets, range, { + includeScheduledInvocations: true, + }); + + for (const dataset of accountDatasets) { + const rows = result.rows[dataset.key] as DatasetRow[] | undefined; + const err = result.errors[dataset.key]; + if (err) { + // Plan-gated datasets (e.g. firewallEventsAdaptiveGroups) return an + // authz error; we don't include those in the registry but if they + // ever get added we want a clear failure here. + throw new Error(`Dataset ${dataset.key} returned error: ${err}`); + } + expect(Array.isArray(rows), `rows for ${dataset.key} should be an array`).toBe(true); + if (!rows) { + continue; + } + for (const row of rows) { + expect(row.dimensions).toBeDefined(); + const timestampDim = dataset.timestampDimension ?? 'datetimeMinute'; + expect(row.dimensions[timestampDim]).toBeDefined(); + for (const [, spec] of Object.entries(dataset.fields)) { + const [block, key] = spec.source; + if (block === '_top') { + continue; + } + const blockData = (row as unknown as Record | undefined>)[block]; + if (blockData && key in blockData) { + const value = blockData[key]; + expect(value === null || typeof value === 'number', `${dataset.key}.${block}.${key}`).toBe(true); + } + } + } + } + expect(result.scheduledInvocations).toBeDefined(); + }, + INTEGRATION_TIMEOUT_MS, + ); + + it( + 'runs the full collector against the live account and emits measurements', + async () => { + const provider = new RecordingProvider(); + const metrics = buildMetricsRepo(provider); + const collector = new CloudflareMetricsCollector(client, env.CLOUDFLARE_ACCOUNT_ID ?? '', metrics, { + // Narrower window — the goal is to verify the full pipeline works, not + // to fetch a full day of data. + lagMs: 5 * 60 * 1000, + windowMs: 60 * 60 * 1000, + }); + const results = await collector.collectAll(ALL_DATASETS); + + // At least one dataset should succeed against our account. + const successes = results.filter((r) => !r.error); + expect(successes.length).toBeGreaterThan(0); + + // Every emitted metric should have a timestamp and an account_id tag. + const exported = provider.metrics.filter((m) => m.name.startsWith('cf_')); + for (const metric of exported) { + expect(metric.exportTimestamp).toBeDefined(); + expect(metric.tags.get('account_id')).toBe(env.CLOUDFLARE_ACCOUNT_ID); + } + + console.log( + `[integration] ${successes.length}/${results.length} datasets succeeded, emitted ${exported.length} metrics`, + ); + for (const result of results) { + if (result.error) { + console.warn(`[integration] ${result.dataset} error: ${result.error}`); + } + } + }, + INTEGRATION_TIMEOUT_MS, + ); +}); + +describe.skipIf(hasCredentials)('Cloudflare GraphQL integration (skipped)', () => { + it('is skipped when CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are unset', () => { + expect(hasCredentials).toBe(false); + }); +}); diff --git a/apps/cloudflare-metrics/src/metric-providers.test.ts b/apps/cloudflare-metrics/src/metric-providers.test.ts new file mode 100644 index 0000000..3162c8f --- /dev/null +++ b/apps/cloudflare-metrics/src/metric-providers.test.ts @@ -0,0 +1,157 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { __resetFlushStateForTests } from './flush-state.js'; +import { InfluxMetricsProvider } from './metric-providers.js'; +import { Metric } from './metric.js'; + +function getLines(provider: InfluxMetricsProvider): string[] { + return (provider as unknown as { metrics: string[] }).metrics; +} + +describe('InfluxDB line protocol escaping', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + it('escapes commas in measurement names', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('my,measurement').intField('v', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toMatch(/^my\\,measurement /); + }); + + it('escapes spaces in measurement names', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('my measurement').intField('v', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toMatch(/^my\\ measurement /); + }); + + it('escapes commas, equals, and spaces in tag keys', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').addTag('k,e=y s', 'val').intField('v', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toContain(String.raw`k\,e\=y\ s=val`); + }); + + it('escapes commas, equals, and spaces in tag values', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').addTag('key', 'a,b=c d').intField('v', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toContain(String.raw`key=a\,b\=c\ d`); + }); + + it('escapes commas, equals, and spaces in field keys', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').intField('f,i=e d', 42); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toContain(String.raw`f\,i\=e\ d=42i`); + }); + + it('handles measurement names with no special characters', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('simple_metric').intField('v', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toMatch(/^simple_metric /); + }); +}); + +describe('InfluxDB line protocol NaN/Infinity handling', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + it('skips NaN float fields', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').floatField('bad', Number.NaN).intField('good', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).not.toContain('bad'); + expect(line).toContain('good=1i'); + }); + + it('skips Infinity float fields', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').floatField('bad', Infinity).intField('good', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).not.toContain('bad'); + expect(line).toContain('good=1i'); + }); + + it('skips -Infinity int fields', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').intField('bad', -Infinity).intField('good', 2); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).not.toContain('bad'); + expect(line).toContain('good=2i'); + }); + + it('drops the entire line when all fields are NaN/Infinity', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').floatField('a', Number.NaN).floatField('b', Infinity); + provider.pushMetric(metric); + expect(getLines(provider)).toHaveLength(0); + }); + + it('keeps zero and negative values', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').intField('zero', 0).intField('neg', -5).floatField('frac', 0); + provider.pushMetric(metric); + const [line] = getLines(provider); + expect(line).toContain('zero=0i'); + expect(line).toContain('neg=-5i'); + expect(line).toContain('frac=0'); + }); +}); + +describe('InfluxDB line protocol timestamp', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + it('appends nanosecond timestamp when exportTimestamp is set', () => { + const provider = new InfluxMetricsProvider('', ''); + const ts = new Date('2026-04-10T12:00:00Z'); + const metric = Metric.create('test').intField('v', 1).setExportTimestamp(ts); + provider.pushMetric(metric); + const [line] = getLines(provider); + const expectedNs = ts.getTime() * 1_000_000; + expect(line.endsWith(` ${expectedNs}`)).toBe(true); + }); + + it('omits timestamp when exportTimestamp is not set', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('test').intField('v', 1); + provider.pushMetric(metric); + const [line] = getLines(provider); + // Line should end with a field value, not a timestamp + expect(line).toMatch(/v=1i$/); + }); +}); + +describe('getMetricsWriteUrl', () => { + it('uses prod URL for prod environment', () => { + const provider = new InfluxMetricsProvider('token', 'prod'); + const url = (provider as unknown as { writeUrl: string }).writeUrl; + expect(url).toBe('https://cf-workers.monitoring.immich.cloud/write'); + }); + + it('uses dev URL for empty environment', () => { + const provider = new InfluxMetricsProvider('token', ''); + const url = (provider as unknown as { writeUrl: string }).writeUrl; + expect(url).toBe('https://cf-workers.monitoring.dev.immich.cloud/write'); + }); + + it('uses staging URL for staging environment', () => { + const provider = new InfluxMetricsProvider('token', 'staging'); + const url = (provider as unknown as { writeUrl: string }).writeUrl; + expect(url).toBe('https://cf-workers.monitoring.staging.immich.cloud/write'); + }); +}); diff --git a/apps/cloudflare-metrics/src/metric-providers.ts b/apps/cloudflare-metrics/src/metric-providers.ts new file mode 100644 index 0000000..b559537 --- /dev/null +++ b/apps/cloudflare-metrics/src/metric-providers.ts @@ -0,0 +1,191 @@ +import { + type LastFlushStats, + MAX_PENDING_FLUSH_BYTES, + pendingFlushBuffers, + recordLastFlushStats, +} from './flush-state.js'; +import { Metric } from './metric.js'; + +export interface IMetricsProviderRepository { + pushMetric(metric: Metric): void; + flush(): void | Promise; +} + +export class HeaderMetricsProvider implements IMetricsProviderRepository { + private _metrics: string[] = []; + + pushMetric(metric: Metric) { + for (const [label, { value, type }] of metric.fields) { + if (type === 'duration') { + const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; + this._metrics.push(`${metric.name}${suffix};dur=${value}`); + } + } + } + + getTimingHeader() { + return this._metrics.join(', '); + } + + flush() { + // Header metrics are consumed via getTimingHeader(); nothing to log. + } +} + +// InfluxDB line protocol escape rules. Keep these simple — we do a cheap +// `includes` check first and only fall into the replace when needed. +// Measurement: escape `,` and ` `. +// Tag key / tag value / field key: escape `,`, `=`, ` `. +function escapeMeasurement(s: string): string { + if (!s.includes(',') && !s.includes(' ')) { + return s; + } + return s.replaceAll(',', String.raw`\,`).replaceAll(' ', String.raw`\ `); +} + +function escapeTagKey(s: string): string { + if (!s.includes(',') && !s.includes('=') && !s.includes(' ')) { + return s; + } + return s + .replaceAll(',', String.raw`\,`) + .replaceAll('=', String.raw`\=`) + .replaceAll(' ', String.raw`\ `); +} + +function escapeTagValue(s: string): string { + if (!s.includes(',') && !s.includes('=') && !s.includes(' ')) { + return s; + } + return s + .replaceAll(',', String.raw`\,`) + .replaceAll('=', String.raw`\=`) + .replaceAll(' ', String.raw`\ `); +} + +function getMetricsWriteUrl(environment: string): string { + if (environment === 'prod') { + return 'https://cf-workers.monitoring.immich.cloud/write'; + } + return `https://cf-workers.monitoring.${environment || 'dev'}.immich.cloud/write`; +} + +export class InfluxMetricsProvider implements IMetricsProviderRepository { + private metrics: string[] = []; + private writeUrl: string; + + constructor( + private influxApiToken: string, + private environment: string, + ) { + this.writeUrl = getMetricsWriteUrl(environment); + } + + pushMetric(metric: Metric) { + // Build InfluxDB line protocol directly to avoid the overhead of the + // Point class (validation, type coercion, repeated escaping passes) on + // the hot path. Our inputs are already well-formed — no string fields, + // no nulls, no duplicate keys. + let line = escapeMeasurement(metric.name); + for (const [key, value] of metric.tags) { + line += ',' + escapeTagKey(key) + '=' + escapeTagValue(value); + } + const fieldParts: string[] = []; + for (const [key, { value, type }] of metric.fields) { + if (!Number.isFinite(value)) { + continue; + } + // int/duration → write as integer with `i` suffix + const serialized = type === 'float' ? value.toString() : Math.round(value).toString() + 'i'; + fieldParts.push(escapeTagKey(key) + '=' + serialized); + } + const fieldPart = fieldParts.join(','); + if (!fieldPart) { + return; // no fields → skip + } + line += ' ' + fieldPart; + const exportTimestamp = metric.exportTimestamp; + if (exportTimestamp) { + // Line protocol uses nanosecond precision + line += ' ' + (exportTimestamp.getTime() * 1_000_000).toString(); + } + this.metrics.push(line); + } + + get pendingCount(): number { + return this.metrics.length; + } + + async flush() { + const startedAt = performance.now(); + const currentBody = this.metrics.join('\n'); + this.metrics = []; + + // Keep stashed retries separate until the POST succeeds so the + // eviction loop can drop oldest-first when the byte cap is crossed. + const parts: string[] = [...pendingFlushBuffers]; + if (currentBody) { + parts.push(currentBody); + } + if (parts.length === 0) { + return; + } + const body = parts.join('\n'); + let status: 'ok' | 'error' = 'ok'; + + // Non-prod body logging removed — payloads of ~3000 lines per tick + // blow the 256KB log budget and waste CPU on string serialization. + if (!this.influxApiToken) { + // No token (local/dev): treat as successful for cleanup. + pendingFlushBuffers.length = 0; + this.recordFlushStats(body, performance.now() - startedAt, status); + return; + } + try { + const response = await fetch(this.writeUrl, { + method: 'POST', + body, + headers: { Authorization: `Token ${this.influxApiToken}` }, + }); + await response.body?.cancel(); + if (response.ok) { + pendingFlushBuffers.length = 0; + } else { + console.error('Failed to push metrics', response.status, response.statusText); + if (currentBody) { + this.stashFailedFlush(currentBody); + } + status = 'error'; + } + } catch (error) { + console.error('Metric flush threw', error); + if (currentBody) { + this.stashFailedFlush(currentBody); + } + status = 'error'; + } + + this.recordFlushStats(body, performance.now() - startedAt, status); + } + + private recordFlushStats(body: string, durationMs: number, status: 'ok' | 'error'): void { + const stats: LastFlushStats = { + bytes: body.length, + durationMs, + status, + pendingBuffers: pendingFlushBuffers.length, + pendingBytes: pendingFlushBuffers.reduce((acc, b) => acc + b.length, 0), + }; + recordLastFlushStats(stats); + } + + private stashFailedFlush(body: string): void { + pendingFlushBuffers.push(body); + let total = pendingFlushBuffers.reduce((acc, b) => acc + b.length, 0); + while (total > MAX_PENDING_FLUSH_BYTES && pendingFlushBuffers.length > 1) { + const dropped = pendingFlushBuffers.shift(); + total -= dropped?.length ?? 0; + console.warn('[metrics] dropped buffered flush body over cap'); + } + } +} diff --git a/apps/cloudflare-metrics/src/metric.ts b/apps/cloudflare-metrics/src/metric.ts new file mode 100644 index 0000000..c826112 --- /dev/null +++ b/apps/cloudflare-metrics/src/metric.ts @@ -0,0 +1,73 @@ +export type MetricFieldType = 'int' | 'float' | 'duration'; + +export class Metric { + private _tags = new Map(); + private _timestamp = performance.now(); + private _exportTimestamp: Date | undefined; + private _fields = new Map(); + private constructor(private _name: string) {} + + static create(name: string) { + return new Metric(name); + } + + get tags() { + return this._tags; + } + + get timestamp() { + return this._timestamp; + } + + get exportTimestamp() { + return this._exportTimestamp; + } + + get fields() { + return this._fields; + } + + get name() { + return this._name; + } + + prefixName(prefix: string) { + if (!this._name.startsWith(`${prefix}_`)) { + this._name = `${prefix}_${this._name}`; + } + } + + addTag(key: string, value: string) { + this._tags.set(key, value); + return this; + } + + addTags(tags: Record) { + for (const [key, value] of Object.entries(tags)) { + this._tags.set(key, value); + } + return this; + } + + durationField(key: string, duration?: number) { + this._fields.set(key, { value: duration ?? performance.now() - this._timestamp, type: 'duration' }); + return this; + } + + intField(key: string, value: number) { + this._fields.set(key, { value, type: 'int' }); + return this; + } + + floatField(key: string, value: number) { + this._fields.set(key, { value, type: 'float' }); + return this; + } + + // Override the wall-clock timestamp written to the metrics backend (vs + // _timestamp which is a monotonic anchor for durationField). + setExportTimestamp(date: Date) { + this._exportTimestamp = date; + return this; + } +} diff --git a/apps/cloudflare-metrics/src/metrics.test.ts b/apps/cloudflare-metrics/src/metrics.test.ts new file mode 100644 index 0000000..e65f7fe --- /dev/null +++ b/apps/cloudflare-metrics/src/metrics.test.ts @@ -0,0 +1,222 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { __resetFlushStateForTests, takeLastFlushStats } from './flush-state.js'; +import { InfluxMetricsProvider, type IMetricsProviderRepository } from './metric-providers.js'; +import { Metric } from './metric.js'; +import { CloudflareMetricsRepository } from './metrics.js'; + +describe('Metric', () => { + it('stores int, float, and duration fields with their types', () => { + const metric = Metric.create('test') + .addTag('foo', 'bar') + .intField('count', 5) + .floatField('ratio', 0.42) + .durationField('duration', 12.5); + + expect(metric.name).toBe('test'); + expect(metric.tags.get('foo')).toBe('bar'); + expect(metric.fields.get('count')).toEqual({ value: 5, type: 'int' }); + expect(metric.fields.get('ratio')).toEqual({ value: 0.42, type: 'float' }); + expect(metric.fields.get('duration')).toEqual({ value: 12.5, type: 'duration' }); + }); + + it('records an export timestamp override', () => { + const now = new Date('2026-04-10T12:00:00Z'); + const metric = Metric.create('test').intField('v', 1).setExportTimestamp(now); + expect(metric.exportTimestamp).toEqual(now); + }); + + it('prefixes metric name only if not already prefixed', () => { + const metric = Metric.create('op'); + metric.prefixName('svc'); + metric.prefixName('svc'); + expect(metric.name).toBe('svc_op'); + }); +}); + +describe('CloudflareMetricsRepository', () => { + class Recorder implements IMetricsProviderRepository { + public readonly pushed: Metric[] = []; + pushMetric(metric: Metric) { + this.pushed.push(metric); + } + flush() { + /* noop */ + } + } + + it('prefixes `push()` metrics with the operation prefix and merges default tags', () => { + const provider = new Recorder(); + const repo = new CloudflareMetricsRepository('svc', new Request('https://localhost/test'), [provider], 'test-env'); + repo.push(Metric.create('event').addTag('kind', 'foo').intField('count', 1)); + const [metric] = provider.pushed; + expect(metric.name).toBe('svc_event'); + expect(metric.tags.get('kind')).toBe('foo'); + expect(metric.tags.get('environment')).toBe('test-env'); + }); + + it('forwards `pushRaw()` metrics untouched (no prefix, no default tags)', () => { + const provider = new Recorder(); + const repo = new CloudflareMetricsRepository('svc', new Request('https://localhost/test'), [provider], 'test-env'); + repo.pushRaw(Metric.create('cf_event').addTag('kind', 'foo').intField('count', 1)); + const [metric] = provider.pushed; + expect(metric.name).toBe('cf_event'); + expect(metric.tags.get('environment')).toBeUndefined(); + }); +}); + +describe('InfluxMetricsProvider flush self-telemetry', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + it('takeLastFlushStats returns null before any flush has completed', () => { + expect(takeLastFlushStats()).toBeNull(); + }); + + it('records stats with status=ok after a successful flush', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (() => Promise.resolve(new Response('', { status: 204 }))) as typeof fetch; + try { + const provider = new InfluxMetricsProvider('token', 'prod'); + provider.pushMetric(Metric.create('foo').intField('v', 1)); + await provider.flush(); + const stats = takeLastFlushStats(); + expect(stats?.status).toBe('ok'); + expect(stats?.bytes).toBeGreaterThan(0); + expect(stats?.pendingBuffers).toBe(0); + expect(stats?.pendingBytes).toBe(0); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('records stats with status=error after a failed flush', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (() => Promise.resolve(new Response('bad', { status: 502 }))) as typeof fetch; + try { + const provider = new InfluxMetricsProvider('token', 'prod'); + provider.pushMetric(Metric.create('foo').intField('v', 1)); + await provider.flush(); + const stats = takeLastFlushStats(); + expect(stats?.status).toBe('error'); + // The failed body gets stashed in the retry buffer — surface that + // so the next-tick self-telemetry can show it. + expect(stats?.pendingBuffers).toBe(1); + expect(stats?.pendingBytes).toBeGreaterThan(0); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('clears lastFlushStats after a single take', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (() => Promise.resolve(new Response('', { status: 204 }))) as typeof fetch; + try { + const provider = new InfluxMetricsProvider('token', 'prod'); + provider.pushMetric(Metric.create('foo').intField('v', 1)); + await provider.flush(); + expect(takeLastFlushStats()).not.toBeNull(); + expect(takeLastFlushStats()).toBeNull(); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('evicts the oldest stashed flush body when the total exceeds the 10 MB cap', async () => { + const originalFetch = globalThis.fetch; + // Always fail so every flush stashes its body in the retry buffer. + globalThis.fetch = (() => Promise.resolve(new Response('', { status: 502 }))) as typeof fetch; + try { + // Each payload is ~4 MB of raw line protocol; three of them land us + // over the 10 MB cap and force an eviction. + const padding = 'x'.repeat(4 * 1024 * 1024); + const provider = new InfluxMetricsProvider('token', 'prod'); + + provider.pushMetric(Metric.create('first').addTag('pad', padding).intField('v', 1)); + await provider.flush(); + + provider.pushMetric(Metric.create('second').addTag('pad', padding).intField('v', 2)); + await provider.flush(); + + provider.pushMetric(Metric.create('third').addTag('pad', padding).intField('v', 3)); + await provider.flush(); + + const stats = takeLastFlushStats(); + // Two buffers should remain — the oldest got dropped to stay under the cap. + expect(stats?.pendingBuffers).toBe(2); + expect(stats?.pendingBytes).toBeLessThan(10 * 1024 * 1024 + 4 * 1024 * 1024); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); + +describe('InfluxMetricsProvider line protocol', () => { + beforeEach(() => { + __resetFlushStateForTests(); + }); + + it('emits int and float fields with the correct suffixes', () => { + const provider = new InfluxMetricsProvider('', ''); + const metric = Metric.create('cf_test') + .addTag('script_name', 'hello') + .intField('requests', 3) + .floatField('duration_ms', 1.5) + .setExportTimestamp(new Date('2026-04-10T12:00:00Z')); + provider.pushMetric(metric); + + // Access the private `metrics` via casting — we just want to assert the line protocol. + const lines = (provider as unknown as { metrics: string[] }).metrics; + expect(lines).toHaveLength(1); + // Integer field should end with `i`, float should not. + expect(lines[0]).toMatch(/cf_test,script_name=hello .*requests=3i/); + expect(lines[0]).toMatch(/duration_ms=1\.5/); + // Timestamp in nanoseconds for 2026-04-10T12:00:00Z + const expectedNs = new Date('2026-04-10T12:00:00Z').getTime() * 1_000_000; + expect(lines[0]).toMatch(new RegExp(` ${expectedNs}$`)); + }); + + it('flush clears the pending buffer', async () => { + const provider = new InfluxMetricsProvider('', ''); + provider.pushMetric(Metric.create('foo').intField('v', 1)); + expect(provider.pendingCount).toBe(1); + await provider.flush(); + expect(provider.pendingCount).toBe(0); + }); + + it('stashes a failed flush body and resends it on the next successful flush', async () => { + // Use a fetch mock via globalThis so the provider picks it up through its + // real `fetch` call path. + const originalFetch = globalThis.fetch; + let call = 0; + const received: string[] = []; + globalThis.fetch = ((_url: RequestInfo | URL, init?: RequestInit) => { + call++; + received.push(init?.body as string); + if (call === 1) { + // First flush: backend is down. + return Promise.resolve(new Response('bad gateway', { status: 502 })); + } + return Promise.resolve(new Response('', { status: 204 })); + }) as typeof fetch; + + try { + const provider = new InfluxMetricsProvider('token', 'prod'); + provider.pushMetric(Metric.create('first').intField('v', 1)); + await provider.flush(); + // First attempt failed; the body should be stashed in the module-level + // retry buffer. + + provider.pushMetric(Metric.create('second').intField('v', 2)); + await provider.flush(); + + expect(call).toBe(2); + // Second request body should contain BOTH the retried first flush and + // the newly pushed metric, concatenated. + expect(received[1]).toContain('first'); + expect(received[1]).toContain('second'); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); diff --git a/apps/cloudflare-metrics/src/metrics.ts b/apps/cloudflare-metrics/src/metrics.ts new file mode 100644 index 0000000..e0ab54f --- /dev/null +++ b/apps/cloudflare-metrics/src/metrics.ts @@ -0,0 +1,66 @@ +import type { IMetricsProviderRepository } from './metric-providers.js'; +import { Metric } from './metric.js'; +import { type AsyncFn, type MonitorOptions, type Operation, monitorAsyncFunction } from './monitor.js'; + +export interface IMetricsRepository { + monitorAsyncFunction( + operation: Operation, + call: T, + options?: MonitorOptions, + ): (...args: Parameters) => Promise>>; + push(metric: Metric): void; +} + +export class CloudflareMetricsRepository implements IMetricsRepository { + private readonly defaultTags: Record; + + constructor( + private operationPrefix: string, + request: Request, + private metricsProviders: IMetricsProviderRepository[], + environment?: string, + ) { + const cf = request.cf as IncomingRequestCfProperties | undefined; + this.defaultTags = { + environment: environment ?? '', + continent: cf?.continent ?? '', + colo: cf?.colo ?? '', + asOrg: cf?.asOrganization ?? '', + }; + } + + monitorAsyncFunction( + operation: Operation, + call: T, + options: MonitorOptions = {}, + ): (...args: Parameters) => Promise>> { + operation = { ...operation, tags: { ...operation.tags, ...this.defaultTags } }; + return monitorAsyncFunction( + this.operationPrefix, + operation, + call, + (metric) => { + for (const provider of this.metricsProviders) { + provider.pushMetric(metric); + } + }, + options, + ); + } + + push(metric: Metric) { + metric.prefixName(this.operationPrefix); + metric.addTags(this.defaultTags); + for (const provider of this.metricsProviders) { + provider.pushMetric(metric); + } + } + + // Push without prefixing or merging default tags — for Cloudflare analytics + // data where measurement names and tags come from the upstream dataset. + pushRaw(metric: Metric) { + for (const provider of this.metricsProviders) { + provider.pushMetric(metric); + } + } +} diff --git a/apps/cloudflare-metrics/src/monitor.ts b/apps/cloudflare-metrics/src/monitor.ts new file mode 100644 index 0000000..bd62f06 --- /dev/null +++ b/apps/cloudflare-metrics/src/monitor.ts @@ -0,0 +1,39 @@ +import { Metric } from './metric.js'; + +export type AsyncFn = (...args: any[]) => Promise; +export type Class = { new (...args: any[]): any }; +export type Operation = { name: string; tags?: Record }; +export type MonitorOptions = { monitorInvocations?: boolean; acceptedErrors?: Class[] }; + +export function monitorAsyncFunction( + operationPrefix: string, + operation: Operation, + call: T, + metricsWriteCallback: (metric: Metric) => void, + options: MonitorOptions = {}, +): (...args: Parameters) => Promise>> { + const { name: operationName, tags = {} } = operation; + const { monitorInvocations = true, acceptedErrors = [] } = options; + + return async (...args: Parameters) => { + const metric = Metric.create(`${operationPrefix}_${operationName}`); + metric.addTags(tags); + + if (monitorInvocations) { + metric.intField('invocation', 1); + } + + try { + return await call(...args); + } catch (error) { + if (!acceptedErrors.some((acceptedError) => error instanceof acceptedError)) { + console.error(error, `${operationName}_errors`); + metric.intField('errors', 1); + } + throw error; + } finally { + metric.durationField('duration'); + metricsWriteCallback(metric); + } + }; +} diff --git a/apps/cloudflare-metrics/src/resource-cache.test.ts b/apps/cloudflare-metrics/src/resource-cache.test.ts new file mode 100644 index 0000000..683e908 --- /dev/null +++ b/apps/cloudflare-metrics/src/resource-cache.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { __resetResourceCachesForTests, ResourceCacheService } from './resource-cache.js'; +import { buildMetricsRepo, FakeRestClient, RecordingProvider, ThrowingRestClient } from './test-helpers.js'; + +const now = () => new Date('2026-04-10T12:00:00Z'); +const laterNow = () => new Date('2026-04-10T12:30:00Z'); + +describe('ResourceCacheService', () => { + let provider: RecordingProvider; + let metrics: ReturnType; + + beforeEach(() => { + provider = new RecordingProvider(); + metrics = buildMetricsRepo(provider); + __resetResourceCachesForTests(); + }); + + it('populates D1, queue, and zone caches from the rest client', async () => { + const restClient = new FakeRestClient( + [{ uuid: 'db-1', name: 'my-db' }], + [{ queue_id: 'q-1', queue_name: 'my-queue' }], + [{ id: 'zone-1', name: 'example.com' }], + ); + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + const cache = service.getCache(); + expect(cache.d1Databases.get('db-1')).toBe('my-db'); + expect(cache.queues.get('q-1')).toBe('my-queue'); + expect(cache.zones.get('zone-1')).toBe('example.com'); + expect(cache.bulkZoneTags.has('zone-1')).toBe(true); + }); + + it('returns empty cache when no rest client is provided', async () => { + const service = new ResourceCacheService('acct', metrics, now); + await service.populate(); + const cache = service.getCache(); + expect(cache.d1Databases.size).toBe(0); + expect(cache.queues.size).toBe(0); + expect(cache.zones.size).toBe(0); + }); + + it('uses cached results on subsequent calls within TTL', async () => { + let callCount = 0; + const restClient = new FakeRestClient([{ uuid: 'db-1', name: 'my-db' }], [], []); + const origList = restClient.listD1Databases.bind(restClient); + restClient.listD1Databases = async (...args: Parameters) => { + callCount++; + return origList(...args); + }; + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + await service.populate(); + expect(callCount).toBe(1); + }); + + it('falls back to stale global cache when rest client throws', async () => { + const restClient = new FakeRestClient( + [{ uuid: 'db-1', name: 'my-db' }], + [{ queue_id: 'q-1', queue_name: 'my-queue' }], + [], + ); + const service1 = new ResourceCacheService('acct', metrics, now, restClient); + await service1.populate(); + + // Reset per-call caches so next populate will re-fetch, then use a throwing client + __resetResourceCachesForTests(); + // Re-seed the global caches by populating once more with good data + const service1b = new ResourceCacheService('acct', metrics, now, restClient); + await service1b.populate(); + + // Now advance past TTL and use throwing client + const throwingClient = new ThrowingRestClient(); + const service2 = new ResourceCacheService('acct', metrics, laterNow, throwingClient); + await service2.populate(); + const cache = service2.getCache(); + expect(cache.d1Databases.get('db-1')).toBe('my-db'); + expect(cache.queues.get('q-1')).toBe('my-queue'); + }); + + it('emits resource_lookup metrics for each resource type', async () => { + const restClient = new FakeRestClient([{ uuid: 'db-1', name: 'my-db' }], [], [{ id: 'z-1', name: 'example.com' }]); + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + const lookups = provider.metrics.filter((m) => m.name === 'cloudflare_metrics_resource_lookup'); + expect(lookups).toHaveLength(3); + const resources = new Set(lookups.map((m) => m.tags.get('resource'))); + expect(resources).toEqual(new Set(['d1_databases', 'queues', 'zones'])); + }); + + it('emits error metrics when rest client throws', async () => { + const throwingClient = new ThrowingRestClient(); + const service = new ResourceCacheService('acct', metrics, now, throwingClient); + await service.populate(); + const lookups = provider.metrics.filter((m) => m.name === 'cloudflare_metrics_resource_lookup'); + for (const lookup of lookups) { + expect(lookup.tags.get('status')).toBe('error'); + } + }); + + describe('resolveMissingZones', () => { + it('resolves zones not in the cache via individual lookups', async () => { + const restClient = new FakeRestClient([], [], [], { + 'zone-new': { id: 'zone-new', name: 'new.example.com' }, + }); + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + await service.resolveMissingZones(['zone-new']); + expect(service.getCache().zones.get('zone-new')).toBe('new.example.com'); + }); + + it('skips zones already in cache', async () => { + const restClient = new FakeRestClient([], [], [{ id: 'zone-1', name: 'example.com' }]); + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + await service.resolveMissingZones(['zone-1']); + expect(restClient.getZoneCalls).toHaveLength(0); + }); + + it('caps individual lookups per run', async () => { + const zones: Record = {}; + for (let i = 0; i < 25; i++) { + zones[`z-${i}`] = { id: `z-${i}`, name: `zone${i}.com` }; + } + const restClient = new FakeRestClient([], [], [], zones); + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + await service.resolveMissingZones(Object.keys(zones)); + expect(restClient.getZoneCalls).toHaveLength(20); + }); + + it('does nothing when no rest client is provided', async () => { + const service = new ResourceCacheService('acct', metrics, now); + await service.populate(); + await service.resolveMissingZones(['zone-1']); + expect(service.getCache().zones.size).toBe(0); + }); + + it('emits lookup metrics with partial status on failures', async () => { + const restClient = new FakeRestClient([], [], [], { + 'zone-ok': { id: 'zone-ok', name: 'ok.com' }, + }); + const origGetZone = restClient.getZone.bind(restClient); + restClient.getZone = async (id: string) => { + if (id === 'zone-fail') { + throw new Error('network error'); + } + return origGetZone(id); + }; + const service = new ResourceCacheService('acct', metrics, now, restClient); + await service.populate(); + await service.resolveMissingZones(['zone-ok', 'zone-fail']); + const lookup = provider.metrics.find( + (m) => m.name === 'cloudflare_metrics_resource_lookup' && m.tags.get('resource') === 'zones_individual', + ); + expect(lookup?.tags.get('status')).toBe('partial'); + expect(lookup?.fields.get('resolved')).toEqual({ value: 1, type: 'int' }); + expect(lookup?.fields.get('failed')).toEqual({ value: 1, type: 'int' }); + }); + }); +}); diff --git a/apps/cloudflare-metrics/src/resource-cache.ts b/apps/cloudflare-metrics/src/resource-cache.ts new file mode 100644 index 0000000..8a14ea7 --- /dev/null +++ b/apps/cloudflare-metrics/src/resource-cache.ts @@ -0,0 +1,218 @@ +import type { ICloudflareRestClient } from './cloudflare-api.js'; +import { Metric } from './metric.js'; +import type { CloudflareMetricsRepository } from './metrics.js'; + +export interface ResourceCache { + d1Databases: Map; + queues: Map; + zones: Map; + // Zones from the bulk /zones list (real account zones). Zone-scoped + // datasets only iterate this subset to avoid querying Pages project + // zones, which would blow through the Workers subrequest limit. + bulkZoneTags: Set; +} + +export function emptyResourceCache(): ResourceCache { + return { + d1Databases: new Map(), + queues: new Map(), + zones: new Map(), + bulkZoneTags: new Set(), + }; +} + +// Module-level caches survive across isolate invocations (minutes to hours), +// avoiding redundant REST lookups on every 1-minute cron tick. +const globalZoneNameCache = new Map(); +const globalD1NameCache = new Map(); +const globalQueueNameCache = new Map(); + +interface CachedResourceLookup { + values: T[]; + loadedAt: number; +} + +const RESOURCE_CACHE_TTL_MS = 10 * 60 * 1000; +let cachedD1Databases: CachedResourceLookup<{ uuid: string; name: string }> | null = null; +let cachedQueues: CachedResourceLookup<{ queue_id: string; queue_name: string }> | null = null; +let cachedBulkZones: CachedResourceLookup<{ id: string; name: string }> | null = null; + +// Cap per-tick individual /zones/{id} lookups to stay under the 50 +// subrequest limit; the global cache absorbs the rest across ticks. +const MAX_INDIVIDUAL_ZONE_LOOKUPS_PER_RUN = 20; + +export function __resetResourceCachesForTests(): void { + globalZoneNameCache.clear(); + globalD1NameCache.clear(); + globalQueueNameCache.clear(); + cachedD1Databases = null; + cachedQueues = null; + cachedBulkZones = null; +} + +export class ResourceCacheService { + private cache: ResourceCache = emptyResourceCache(); + + constructor( + private readonly accountTag: string, + private readonly metrics: CloudflareMetricsRepository, + private readonly now: () => Date, + private readonly restClient?: ICloudflareRestClient, + ) {} + + getCache(): ResourceCache { + return this.cache; + } + + async populate(): Promise { + this.cache = emptyResourceCache(); + for (const [tag, name] of globalZoneNameCache) { + this.cache.zones.set(tag, name); + } + if (!this.restClient) { + return; + } + + const restClient = this.restClient; + const now = this.now().getTime(); + const cacheFresh = (cache: CachedResourceLookup | null): cache is CachedResourceLookup => + cache !== null && now - cache.loadedAt < RESOURCE_CACHE_TTL_MS; + + const [d1Result, queuesResult, zonesResult] = await Promise.allSettled([ + cacheFresh(cachedD1Databases) + ? Promise.resolve(cachedD1Databases.values) + : restClient.listD1Databases(this.accountTag).then((values) => { + cachedD1Databases = { values, loadedAt: now }; + return values; + }), + cacheFresh(cachedQueues) + ? Promise.resolve(cachedQueues.values) + : restClient.listQueues(this.accountTag).then((values) => { + cachedQueues = { values, loadedAt: now }; + return values; + }), + cacheFresh(cachedBulkZones) + ? Promise.resolve(cachedBulkZones.values) + : restClient.listZones(this.accountTag).then((values) => { + cachedBulkZones = { values, loadedAt: now }; + return values; + }), + ]); + + this.recordResourceLookup('d1_databases', d1Result, (items) => { + for (const db of items) { + if (db.uuid && db.name) { + this.cache.d1Databases.set(db.uuid, db.name); + globalD1NameCache.set(db.uuid, db.name); + } + } + }); + this.recordResourceLookup('queues', queuesResult, (items) => { + for (const q of items) { + if (q.queue_id && q.queue_name) { + this.cache.queues.set(q.queue_id, q.queue_name); + globalQueueNameCache.set(q.queue_id, q.queue_name); + } + } + }); + this.recordResourceLookup('zones', zonesResult, (items) => { + for (const z of items) { + if (z.id && z.name) { + this.cache.zones.set(z.id, z.name); + this.cache.bulkZoneTags.add(z.id); + } + } + }); + + // Fall back to stale global caches when a REST lookup fails entirely, + // so metric tags still carry names rather than being empty. + if (d1Result.status === 'rejected') { + for (const [uuid, name] of globalD1NameCache) { + this.cache.d1Databases.set(uuid, name); + } + } + if (queuesResult.status === 'rejected') { + for (const [id, name] of globalQueueNameCache) { + this.cache.queues.set(id, name); + } + } + } + + async resolveMissingZones(missingTags: Iterable): Promise { + if (!this.restClient) { + return; + } + const unique = new Set(); + for (const tag of missingTags) { + if (tag && !this.cache.zones.has(tag)) { + unique.add(tag); + } + } + if (unique.size === 0) { + return; + } + const ids = [...unique].slice(0, MAX_INDIVIDUAL_ZONE_LOOKUPS_PER_RUN); + const startedAt = performance.now(); + const restClient = this.restClient; + const results = await Promise.allSettled(ids.map((id) => restClient.getZone(id))); + let resolved = 0; + let failed = 0; + for (const [i, result] of results.entries()) { + if (result.status === 'fulfilled' && result.value) { + this.cache.zones.set(ids[i], result.value.name); + globalZoneNameCache.set(ids[i], result.value.name); + resolved++; + } else if (result.status === 'rejected') { + failed++; + } + } + this.metrics.push( + Metric.create('resource_lookup') + .addTag('resource', 'zones_individual') + .addTag('status', failed > 0 ? 'partial' : 'success') + .intField('requested', ids.length) + .intField('resolved', resolved) + .intField('failed', failed) + .durationField('duration', performance.now() - startedAt), + ); + } + + private recordResourceLookup( + resource: string, + result: PromiseSettledResult, + onSuccess: (items: T[]) => void, + ): void { + if (result.status === 'fulfilled') { + onSuccess(result.value); + this.metrics.push( + Metric.create('resource_lookup') + .addTag('resource', resource) + .addTag('status', 'success') + .intField('count', result.value.length), + ); + } else { + console.error(`[collector] resource lookup ${resource} failed:`, errorMessage(result.reason)); + this.metrics.push( + Metric.create('resource_lookup') + .addTag('resource', resource) + .addTag('status', 'error') + .addTag('error', errorTag(result.reason)) + .intField('errors', 1), + ); + } + } +} + +function errorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +function errorTag(error: unknown): string { + if (error instanceof Error) { + return error.name; + } + return 'unknown'; +} diff --git a/apps/cloudflare-metrics/src/scheduled.test.ts b/apps/cloudflare-metrics/src/scheduled.test.ts new file mode 100644 index 0000000..7e7c222 --- /dev/null +++ b/apps/cloudflare-metrics/src/scheduled.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; + +// The scheduled handler uses module-level state (isolateStartedAt, lastSuccessfulEndMs) +// and constructs real clients, making it hard to unit test directly. Instead we test +// the pure helper `computeWindowMs` which drives the backfill logic. +// +// computeWindowMs is not exported, so we replicate its logic here to verify the +// algorithm. The constants must stay in sync with scheduled.ts. + +const DEFAULT_LAG_MS = 5 * 60 * 1000; +const DEFAULT_WINDOW_MS = 5 * 60 * 1000; +const MAX_BACKFILL_MS = 30 * 60 * 1000; + +function computeWindowMs(isColdStart: boolean, lastSuccessfulEndMs: number | null, scheduledMs: number): number { + if (lastSuccessfulEndMs !== null) { + const gapMs = scheduledMs - DEFAULT_LAG_MS - lastSuccessfulEndMs; + if (gapMs > DEFAULT_WINDOW_MS) { + return Math.min(gapMs, MAX_BACKFILL_MS); + } + return DEFAULT_WINDOW_MS; + } + if (isColdStart) { + return 15 * 60 * 1000; + } + return DEFAULT_WINDOW_MS; +} + +describe('computeWindowMs', () => { + const baseTime = new Date('2026-04-10T12:00:00Z').getTime(); + + it('returns default window when last end is recent', () => { + const lastEnd = baseTime - DEFAULT_LAG_MS - DEFAULT_WINDOW_MS; + expect(computeWindowMs(false, lastEnd, baseTime)).toBe(DEFAULT_WINDOW_MS); + }); + + it('extends window to cover a gap', () => { + const gapMinutes = 10; + const lastEnd = baseTime - DEFAULT_LAG_MS - gapMinutes * 60 * 1000; + const result = computeWindowMs(false, lastEnd, baseTime); + expect(result).toBe(gapMinutes * 60 * 1000); + }); + + it('caps backfill at MAX_BACKFILL_MS', () => { + const lastEnd = baseTime - DEFAULT_LAG_MS - 60 * 60 * 1000; // 1 hour gap + expect(computeWindowMs(false, lastEnd, baseTime)).toBe(MAX_BACKFILL_MS); + }); + + it('returns 15-minute window on cold start with no prior state', () => { + expect(computeWindowMs(true, null, baseTime)).toBe(15 * 60 * 1000); + }); + + it('returns default window when not cold start and no prior state', () => { + expect(computeWindowMs(false, null, baseTime)).toBe(DEFAULT_WINDOW_MS); + }); + + it('uses scheduledMs (not wall clock) for gap calculation', () => { + const lastEnd = baseTime - DEFAULT_LAG_MS - 8 * 60 * 1000; + // Two different scheduledMs values produce different gaps + const result1 = computeWindowMs(false, lastEnd, baseTime); + const result2 = computeWindowMs(false, lastEnd, baseTime + 2 * 60 * 1000); + expect(result1).toBe(8 * 60 * 1000); + expect(result2).toBe(10 * 60 * 1000); + }); + + it('returns default window when gap equals exactly DEFAULT_WINDOW_MS', () => { + const lastEnd = baseTime - DEFAULT_LAG_MS - DEFAULT_WINDOW_MS; + expect(computeWindowMs(false, lastEnd, baseTime)).toBe(DEFAULT_WINDOW_MS); + }); + + it('returns default window when gap is less than DEFAULT_WINDOW_MS', () => { + const lastEnd = baseTime - DEFAULT_LAG_MS - 2 * 60 * 1000; + expect(computeWindowMs(false, lastEnd, baseTime)).toBe(DEFAULT_WINDOW_MS); + }); +}); diff --git a/apps/cloudflare-metrics/src/test-helpers.ts b/apps/cloudflare-metrics/src/test-helpers.ts new file mode 100644 index 0000000..af43a5c --- /dev/null +++ b/apps/cloudflare-metrics/src/test-helpers.ts @@ -0,0 +1,167 @@ +import type { ICloudflareRestClient } from './cloudflare-api.js'; +import type { CloudflareGraphQLClient } from './graphql-client.js'; +import type { IMetricsProviderRepository } from './metric-providers.js'; +import { Metric } from './metric.js'; +import { CloudflareMetricsRepository } from './metrics.js'; +import type { DatasetQuery, DatasetRow } from './types.js'; + +/** + * Test-only metrics provider that records every pushed metric so tests can + * assert on the emitted measurement name, tags, and fields without having to + * parse the final line protocol. + */ +export class RecordingProvider implements IMetricsProviderRepository { + readonly metrics: Metric[] = []; + flushCount = 0; + pushMetric(metric: Metric) { + this.metrics.push(metric); + } + flush() { + this.flushCount++; + } +} + +/** + * Builds a metrics repository wrapping the given provider with the same + * prefix (`cloudflare_metrics`) used in the production entrypoint. Tests + * that care about the prefix get the same behaviour as real requests. + */ +export function buildMetricsRepo(provider: IMetricsProviderRepository) { + return new CloudflareMetricsRepository('cloudflare_metrics', new Request('https://localhost/test'), [provider], ''); +} + +/** + * Fake GraphQL client that returns pre-seeded rows for account and zone + * datasets. Tests configure it with a map of datasetKey → rows and + * optionally zone-scoped rows keyed by zoneTag. + */ +export class FakeGraphQLClient { + public calls: Array<{ dataset: string; range: { start: Date; end: Date } }> = []; + public zoneCalls: Array<{ zoneTags: string[]; dataset: string }> = []; + public batchCalls: Array<{ datasets: string[]; includeScheduled: boolean }> = []; + + constructor( + public readonly rowsByDataset: Record, + public readonly zoneRowsByDataset: Record> = {}, + public readonly scheduledInvocations: Array<{ + scriptName: string; + cron: string; + status: string; + datetime: string; + cpuTimeUs: number; + }> = [], + public readonly errorsByDataset: Record = {}, + ) {} + + // eslint-disable-next-line @typescript-eslint/require-await + async fetchAccountBatch( + _accountTag: string, + datasets: readonly DatasetQuery[], + range: { start: Date; end: Date }, + options: { includeScheduledInvocations?: boolean } = {}, + ) { + this.batchCalls.push({ + datasets: datasets.map((d) => d.key), + includeScheduled: options.includeScheduledInvocations ?? false, + }); + const rows: Record = {}; + const errors: Record = {}; + for (const dataset of datasets) { + this.calls.push({ dataset: dataset.key, range }); + if (this.errorsByDataset[dataset.key]) { + errors[dataset.key] = this.errorsByDataset[dataset.key]; + continue; + } + rows[dataset.key] = this.rowsByDataset[dataset.key] ?? []; + } + return { + rows, + errors, + scheduledInvocations: options.includeScheduledInvocations ? this.scheduledInvocations : undefined, + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async fetchZoneBatch(zoneTags: readonly string[], dataset: DatasetQuery, _range: { start: Date; end: Date }) { + this.zoneCalls.push({ zoneTags: [...zoneTags], dataset: dataset.key }); + const rows: Record = {}; + const errors: Record = {}; + for (const zoneTag of zoneTags) { + const zoneRows = this.zoneRowsByDataset[dataset.key]?.[zoneTag]; + if (zoneRows === undefined && this.errorsByDataset[`zone:${zoneTag}`]) { + errors[zoneTag] = this.errorsByDataset[`zone:${zoneTag}`]; + continue; + } + rows[zoneTag] = zoneRows ?? []; + } + return { rows, errors }; + } +} + +/** `FakeGraphQLClient` duck-typed as a `CloudflareGraphQLClient` for DI. */ +export function asGraphQLClient(fake: FakeGraphQLClient): CloudflareGraphQLClient { + return fake as unknown as CloudflareGraphQLClient; +} + +/** + * Fake REST client returning pre-seeded D1/queue/zone lookups. The + * individualZones map is consulted by `getZone` so tests can pretend a + * pages-only zone exists outside the bulk list. + */ +export class FakeRestClient implements ICloudflareRestClient { + public getZoneCalls: string[] = []; + constructor( + public readonly d1: Array<{ uuid: string; name: string }> = [], + public readonly queues: Array<{ queue_id: string; queue_name: string }> = [], + public readonly zones: Array<{ id: string; name: string }> = [], + public readonly individualZones: Record = {}, + ) {} + + // eslint-disable-next-line @typescript-eslint/require-await + async listD1Databases() { + return this.d1; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async listQueues() { + return this.queues; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async listZones() { + return this.zones; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async getZone(zoneId: string) { + this.getZoneCalls.push(zoneId); + if (zoneId in this.individualZones) { + return this.individualZones[zoneId]; + } + return null; + } +} + +/** + * Rest client that always throws, used to test error paths on resource + * lookups (e.g. the collector should still emit a self-metric rather than + * crashing the whole cron tick). + */ +export class ThrowingRestClient implements ICloudflareRestClient { + // eslint-disable-next-line @typescript-eslint/require-await + async listD1Databases(): Promise { + throw new Error('d1 boom'); + } + // eslint-disable-next-line @typescript-eslint/require-await + async listQueues(): Promise { + throw new Error('queues boom'); + } + // eslint-disable-next-line @typescript-eslint/require-await + async listZones(): Promise { + throw new Error('zones boom'); + } + // eslint-disable-next-line @typescript-eslint/require-await + async getZone(): Promise { + throw new Error('getZone boom'); + } +} diff --git a/apps/cloudflare-metrics/src/types.ts b/apps/cloudflare-metrics/src/types.ts new file mode 100644 index 0000000..6a45e40 --- /dev/null +++ b/apps/cloudflare-metrics/src/types.ts @@ -0,0 +1,68 @@ +export type FieldValue = number; + +export type FieldType = 'int' | 'float'; + +// `['_top', 'count']` reads row.count directly; `['sum', 'requests']` reads row.sum.requests. +export type FieldSource = readonly [string, string]; + +export interface FieldSpec { + type: FieldType; + source: FieldSource; + scale?: number; +} + +export interface TagSpec { + source: string; + as: string; +} + +export type AggregationBlock = 'sum' | 'avg' | 'max' | 'min' | 'quantiles' | 'uniq'; + +export interface DatasetQuery { + key: string; + measurement: string; + field: string; + scope?: 'account' | 'zone'; + dimensions: readonly string[]; + blocks: Partial>; + topLevelFields?: readonly string[]; + tags: readonly TagSpec[]; + fields: Record; + timestampDimension?: string; + limit?: number; + extraFilter?: Record; +} + +export interface DatasetRow { + dimensions: Record; + sum?: Record; + avg?: Record; + max?: Record; + min?: Record; + quantiles?: Record; + uniq?: Record; + count?: number | null; +} + +export interface GraphQLResponse { + data: T | null; + errors?: Array<{ + message: string; + path?: Array; + extensions?: Record; + }> | null; +} + +export interface AccountQueryResult { + viewer: { + accounts: Array>; + }; +} + +export interface CollectionResult { + dataset: string; + rows: number; + points: number; + durationMs: number; + error?: string; +} diff --git a/apps/cloudflare-metrics/tsconfig.json b/apps/cloudflare-metrics/tsconfig.json new file mode 100644 index 0000000..e26f7b2 --- /dev/null +++ b/apps/cloudflare-metrics/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers", "vitest/globals"], + "baseUrl": ".", + "paths": { + "@immich-services/cloudflare-metrics/*": ["*"] + } + }, + "include": ["src/**/*", "worker-configuration.d.ts"] +} diff --git a/apps/cloudflare-metrics/vitest.config.ts b/apps/cloudflare-metrics/vitest.config.ts new file mode 100644 index 0000000..b319cad --- /dev/null +++ b/apps/cloudflare-metrics/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineWorkersConfig({ + test: { + exclude: ['**/node_modules/**', 'src/integration.test.ts'], + poolOptions: { + workers: { + wrangler: { configPath: './wrangler.toml' }, + miniflare: { + // Inject a fake VictoriaMetrics token so the InfluxMetricsProvider + // takes the real fetch path in tests — otherwise it short-circuits + // in its "no token" branch and we can't observe flush behaviour. + bindings: { + VMETRICS_API_TOKEN: 'test-token', + }, + }, + }, + }, + }, +}); diff --git a/apps/cloudflare-metrics/vitest.integration.config.ts b/apps/cloudflare-metrics/vitest.integration.config.ts new file mode 100644 index 0000000..72d4d32 --- /dev/null +++ b/apps/cloudflare-metrics/vitest.integration.config.ts @@ -0,0 +1,18 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineWorkersConfig({ + test: { + include: ['src/integration.test.ts'], + poolOptions: { + workers: { + wrangler: { configPath: './wrangler.toml' }, + miniflare: { + bindings: { + CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN ?? '', + CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID ?? '', + }, + }, + }, + }, + }, +}); diff --git a/apps/cloudflare-metrics/worker-configuration.d.ts b/apps/cloudflare-metrics/worker-configuration.d.ts new file mode 100644 index 0000000..931a778 --- /dev/null +++ b/apps/cloudflare-metrics/worker-configuration.d.ts @@ -0,0 +1,11 @@ +interface Env { + ENVIRONMENT: string; + CLOUDFLARE_ACCOUNT_ID: string; + CLOUDFLARE_API_TOKEN?: string; + VMETRICS_API_TOKEN?: string; +} + +declare module 'cloudflare:test' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface ProvidedEnv extends Env {} +} diff --git a/apps/cloudflare-metrics/wrangler.toml b/apps/cloudflare-metrics/wrangler.toml new file mode 100644 index 0000000..d14df62 --- /dev/null +++ b/apps/cloudflare-metrics/wrangler.toml @@ -0,0 +1,15 @@ +#:schema node_modules/wrangler/config-schema.json +name = "cloudflare-metrics-immich-app" +main = "src/index.ts" +compatibility_date = "2025-09-16" +compatibility_flags = ["nodejs_compat"] + +[limits] +cpu_ms = 30000 + +[triggers] +crons = ["* * * * *"] + +[vars] +ENVIRONMENT = "" +CLOUDFLARE_ACCOUNT_ID = "" diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/alerts.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/alerts.tf new file mode 100644 index 0000000..da84f0a --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/alerts.tf @@ -0,0 +1,600 @@ +locals { + # Must match the folder_name input passed to `module.grafana` in dashboards.tf. + grafana_folder_name = var.stage != "" ? "cloudflare-metrics (${var.stage})" : "cloudflare-metrics" + # Mirrors the regex inside the shared grafana module + # (devtools//tf/shared/modules/grafana/grafana.tf): lowercase, anything + # that isn't a-z or 0-9 becomes "-". + dashboards_folder_uid = replace(lower(local.grafana_folder_name), "/[^a-z\\d]/", "-") + + prometheus_datasource_uid = "36979063-5384-4eb9-8679-565a727cbc13" + + # The worker's own script name as it appears in cf_workers_invocations. + # Must match the cloudflare_worker.worker.name resource. + worker_script_name = "${var.app_name}-api${local.resource_suffix}" + + # All exporter-health alerts link back to panels on the exporter-health + # dashboard. The panel IDs are pinned in the 9000 range inside the + # dashboard generator (apps/cloudflare-metrics generate-dashboards.py) so + # reordering panels doesn't break the alert → panel links. + exporter_health_uid = "cf-exporter-health" + alert_panels = { + collector_liveness = 9001 + cron_errors = 9002 + dataset_errors = 9003 + flush_errors = 9004 + pending_flush = 9005 + graphql_requests = 9006 + graphql_err_response = 9007 + } +} + +resource "grafana_rule_group" "cloudflare_metrics_alerts" { + org_id = 1 + name = "Cloudflare Metrics Exporter" + folder_uid = local.dashboards_folder_uid + interval_seconds = 60 + disable_provenance = true + + # 1) Collector stopped running entirely. `cron_summary_datasets` is pushed + # unconditionally once per successful tick in the scheduled handler, so + # any gap in the series means the handler is wedged, crashing, or + # missing its token. The threshold never trips in practice — it's just + # there to give the rule a condition; the real signal is NoData → + # Alerting. + rule { + name = "Collector Not Running" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 600 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max_over_time(cloudflare_metrics_cron_summary_datasets[10m])" + instant = true + intervalMs = 60000 + legendFormat = "datasets" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [0], type = "lt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "Alerting" + exec_err_state = "Error" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.collector_liveness) + summary = "Cloudflare metrics collector is not running" + description = "No cloudflare_metrics_cron_summary_datasets datapoints in the last 10 minutes — the scheduled handler is wedged, crashing, or missing its Cloudflare API token." + } + labels = { severity = "1" } + is_paused = false + } + + # 2) Per-tick cron exception. Any cron_error surfaces either a missing + # config or a thrown exception during collection. + rule { + name = "Collector Cron Error" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 600 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(sum_over_time(cloudflare_metrics_cron_error_count[10m]))" + instant = true + intervalMs = 60000 + legendFormat = "errors" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [0], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "1m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.cron_errors) + summary = "Cloudflare metrics collector threw an exception" + description = "cron_error_count was incremented in the last 10 minutes — collection threw or the handler booted without its Cloudflare credentials." + } + labels = { severity = "1" } + is_paused = false + } + + # 3) Sustained per-dataset errors. A handful here and there are expected + # on datasets for products we don't use; this catches real breakage + # like plan changes or renamed fields. + rule { + name = "Collector Dataset Errors Sustained" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 900 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(sum by (dataset) (sum_over_time(cloudflare_metrics_collector_dataset_errors[15m])))" + instant = true + intervalMs = 60000 + legendFormat = "errors" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [10], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.dataset_errors) + summary = "Collector dataset errors sustained" + description = "A single dataset has had more than 10 errors in the last 15 minutes. Check the exporter-health dashboard to see which dataset is failing." + } + labels = { severity = "3" } + is_paused = false + } + + # 4) Flush to VictoriaMetrics is failing. This isn't immediately + # data-loss — failed bodies get stashed and retried on the next tick — + # but sustained failures will eventually hit the 10 MB stash cap and + # drop points. + rule { + name = "Metrics Flush Failing" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 900 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(sum_over_time(cloudflare_metrics_flush_errors[15m]))" + instant = true + intervalMs = 60000 + legendFormat = "errors" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [3], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.flush_errors) + summary = "Metrics flush to VictoriaMetrics is failing" + description = "More than 3 flush errors in the last 15 minutes. The worker is stashing bodies for retry; the stash is capped at 10 MB before it starts dropping the oldest. Check VictoriaMetrics / vmauth health." + } + labels = { severity = "1" } + is_paused = false + } + + # 5) Pending flush buffer growing. Catches cases where failures are + # briefly under the rate threshold but the backlog is still climbing. + rule { + name = "Pending Flush Buffer Growing" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 900 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(cloudflare_metrics_flush_pending_buffers)" + instant = true + intervalMs = 60000 + legendFormat = "buffers" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [3], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.pending_flush) + summary = "Pending flush buffer growing" + description = "The worker has stashed more than 3 failed flush bodies waiting for retry. Something is preventing the VM write endpoint from draining them." + } + labels = { severity = "3" } + is_paused = false + } + + # 6) Subrequest budget near the Workers 50/invocation cap. We currently + # burn ~16 per tick; alert if we ever approach the ceiling so we know + # to re-chunk before hitting the limit in production. + rule { + name = "GraphQL Subrequest Budget Near Cap" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 600 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(cloudflare_metrics_graphql_client_requests)" + instant = true + intervalMs = 60000 + legendFormat = "requests" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [40], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.graphql_requests) + summary = "GraphQL subrequest count approaching Workers cap" + description = "A cron tick issued more than 40 GraphQL subrequests (Workers paid-plan cap is 50). Add more datasets into the same batched chunk or raise ACCOUNT_BATCH_CHUNK_SIZE before hitting the ceiling." + } + labels = { severity = "3" } + is_paused = false + } + + # 7) GraphQL error-response rate. Our batched query shares chunks, so a + # wrong field name surfaces as every dataset in that chunk failing. + # Treat any sustained error-response rate as a deployment smell. + rule { + name = "GraphQL Error Responses Sustained" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 900 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(sum_over_time(cloudflare_metrics_graphql_client_error_responses[15m]))" + instant = true + intervalMs = 60000 + legendFormat = "error responses" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [5], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.graphql_err_response) + summary = "GraphQL error responses sustained" + description = "More than 5 GraphQL responses with errors[] in the last 15 minutes. This usually means a batched chunk contains an invalid field name; check the exporter-health dashboard." + } + labels = { severity = "3" } + is_paused = false + } + + # 8) Collector worker crashing (exceededResources, OOM, etc). This queries + # the Cloudflare-reported error count for our own worker script — data + # that the collector itself writes to VictoriaMetrics from the analytics + # API. Because it's 5-minute-lagged analytics data written by whichever + # tick succeeds, it catches sustained crash loops that our self-telemetry + # alerts miss (since self-telemetry isn't emitted when the handler crashes). + rule { + name = "Collector Worker Crash Rate" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 1800 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(sum_over_time(cf_workers_invocations_errors{script_name=\"${local.worker_script_name}\"}[30m]))" + instant = true + intervalMs = 60000 + legendFormat = "errors" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [3], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "5m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.collector_liveness) + summary = "Collector worker is crashing" + description = "More than 3 cf_workers_invocations_errors for the collector's own script in the last 30 minutes. The worker is likely hitting exceededResources (CPU/memory limit) or another runtime crash. Check the isolate age panel and Cloudflare dashboard for the script." + } + labels = { severity = "1" } + is_paused = false + } + + # 9) CPU time approaching the limit. With the ceiling at 30s, sustained + # p99 > 1s means the worker is doing more work than expected and could + # eventually hit exceededResources again if it keeps climbing. + rule { + name = "Collector CPU Time High" + condition = "C" + + data { + ref_id = "A" + datasource_uid = local.prometheus_datasource_uid + relative_time_range { + from = 900 + to = 0 + } + model = jsonencode({ + datasource = { type = "prometheus", uid = local.prometheus_datasource_uid } + editorMode = "code" + expr = "max(cf_workers_invocations_cpu_time_us_p99{script_name=\"${local.worker_script_name}\"})" + instant = true + intervalMs = 60000 + legendFormat = "cpu_p99" + maxDataPoints = 43200 + refId = "A" + }) + } + data { + ref_id = "C" + datasource_uid = "__expr__" + relative_time_range { + from = 0 + to = 0 + } + model = jsonencode({ + datasource = { type = "__expr__", uid = "__expr__" } + expression = "A" + refId = "C" + type = "threshold" + conditions = [{ + evaluator = { params = [1000000], type = "gt" } + operator = { type = "and" } + query = { params = ["C"] } + reducer = { params = [], type = "last" } + type = "query" + }] + intervalMs = 1000 + maxDataPoints = 43200 + }) + } + + no_data_state = "OK" + exec_err_state = "OK" + for = "10m" + annotations = { + __dashboardUid__ = local.exporter_health_uid + __panelId__ = tostring(local.alert_panels.collector_liveness) + summary = "Collector CPU time is high" + description = "p99 CPU time for the collector worker has been above 1s for 10 minutes. The CPU limit is 30s but sustained high usage may indicate a problem. Check the workers dashboard for the script." + } + labels = { severity = "3" } + is_paused = false + } +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/api-token.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/api-token.tf new file mode 100644 index 0000000..452a844 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/api-token.tf @@ -0,0 +1,84 @@ +# Read-only API token used by the cloudflare-metrics worker to query the +# GraphQL Analytics API and to enumerate resource metadata (D1 databases, +# queues, zones) that's needed to enrich metric tags with human-readable +# names. Scoped to this account only, read-only permissions. +# +# The permission group UUIDs are looked up dynamically via the bootstrap +# provider, which authenticates with the user-level `var.cloudflare_api_token` +# that already has permission to list/manage API tokens. + +data "cloudflare_api_token_permission_groups_list" "all" { + provider = cloudflare.bootstrap +} + +locals { + cloudflare_metrics_permission_group_names = [ + "Account Analytics Read", + "Analytics Read", + "D1 Read", + "Queues Read", + "Zone Read", + ] + + # Some permission group names exist at multiple scopes (e.g. account and + # zone-level "Logs Read"), so we can't build a simple `name => id` map. + # Instead we look up each required name explicitly and take the first match. + cf_permission_group_ids = { + for name in local.cloudflare_metrics_permission_group_names : + name => [ + for g in data.cloudflare_api_token_permission_groups_list.all.result : g.id if g.name == name + ][0] + } +} + +# Bumping this forces the analytics_read token to be destroyed and recreated +# on the next apply. Cloudflare provider v5 has a bug where +# `cloudflare_api_token.value` is only populated immediately after creation +# (see cloudflare/terraform-provider-cloudflare#5045), so we intentionally +# recreate the token whenever we need the value to be freshly available to +# downstream resources. +resource "terraform_data" "analytics_token_generation" { + input = "7" +} + +resource "cloudflare_api_token" "analytics_read" { + provider = cloudflare.bootstrap + + name = "cloudflare-metrics-analytics-read${local.resource_suffix}" + + # Two policies: one scoped to the account (for account-level datasets and + # REST endpoints like D1 and Queues), and one scoped to all zones in the + # account (so `GET /zones/{id}` can resolve individual zone names for + # Cloudflare Pages projects that don't appear in the bulk `/zones` list). + policies = [ + { + effect = "allow" + permission_groups = [ + { id = local.cf_permission_group_ids["Account Analytics Read"] }, + { id = local.cf_permission_group_ids["D1 Read"] }, + { id = local.cf_permission_group_ids["Queues Read"] }, + ] + resources = jsonencode({ + "com.cloudflare.api.account.${var.cloudflare_account_id}" = "*" + }) + }, + { + effect = "allow" + permission_groups = [ + { id = local.cf_permission_group_ids["Zone Read"] }, + { id = local.cf_permission_group_ids["Analytics Read"] }, + ] + resources = jsonencode({ + "com.cloudflare.api.account.zone.*" = "*" + }) + }, + ] + + lifecycle { + replace_triggered_by = [terraform_data.analytics_token_generation] + } +} + +output "analytics_token_id" { + value = cloudflare_api_token.analytics_read.id +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/config.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/config.tf new file mode 100644 index 0000000..c906eeb --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/config.tf @@ -0,0 +1,15 @@ +terraform { + backend "pg" {} + required_version = "~> 1.7" + + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 5" + } + grafana = { + source = "grafana/grafana" + version = "~> 3" + } + } +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards.tf new file mode 100644 index 0000000..8e763ea --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards.tf @@ -0,0 +1,7 @@ +module "grafana" { + source = "git::https://github.com/immich-app/devtools.git//tf/shared/modules/grafana?ref=main" + + folder_name = local.grafana_folder_name + dashboards_path = "./dashboards" + env = var.env +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/00-account-overview.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/00-account-overview.json new file mode 100644 index 0000000..d018ef5 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/00-account-overview.json @@ -0,0 +1,2674 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Key Metrics (24h)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Worker Requests", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Worker Errors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_invocations_errors[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Worker Subrequests", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_subrequests_subrequests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "HTTP Requests (Zones)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_requests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "HTTP Bytes (Zones)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_bytes[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "stat", + "title": "D1 Queries", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_detail_query_count[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 8, + "type": "stat", + "title": "R2 Operations", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_r2_operations_requests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 9, + "type": "stat", + "title": "KV Operations", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_kv_operations_requests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 10, + "type": "stat", + "title": "Queue Operations", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 11, + "type": "stat", + "title": "Durable Object Requests", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 12, + "type": "stat", + "title": "Scheduled Invocations", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_scheduled_invocations[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 13, + "type": "stat", + "title": "Hyperdrive Queries", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_hyperdrive_queries_count[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 14, + "type": "row", + "title": "Workers", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "panels": [] + }, + { + "id": 15, + "type": "timeseries", + "title": "Worker Request per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_invocations_requests[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 16, + "type": "timeseries", + "title": "Worker Error per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_invocations_errors[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 17, + "type": "row", + "title": "HTTP (Zones)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "panels": [] + }, + { + "id": 18, + "type": "timeseries", + "title": "Requests per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_http_requests_overview_requests[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 19, + "type": "timeseries", + "title": "Cache Hit per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_http_requests_overview_cached_requests[1m])) / clamp_min(sum by (zone_name) (max_over_time(cf_http_requests_overview_requests[1m])), 1)", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 20, + "type": "row", + "title": "Storage & Resources", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "panels": [] + }, + { + "id": 21, + "type": "timeseries", + "title": "D1 Query per Database", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 28 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (database_name) (max_over_time(cf_d1_queries_detail_query_count[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 22, + "type": "timeseries", + "title": "R2 Storage (stored bytes)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 28 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (bucket_name) (cf_r2_storage_payload_bytes))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{bucket_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 23, + "type": "timeseries", + "title": "KV Key Count", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 28 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (namespace_id) (cf_kv_storage_key_count))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{namespace_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 24, + "type": "row", + "title": "Exporter Health", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 49 + }, + "panels": [] + }, + { + "id": 25, + "type": "timeseries", + "title": "Collector Errors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 50 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (dataset, error, error_scope) (cloudflare_metrics_collector_dataset_errors)", + "instant": false, + "range": true, + "legendFormat": "{{dataset}} ({{error_scope}}: {{error}})", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 26, + "type": "timeseries", + "title": "Rows Collected per Dataset", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 50 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (dataset) (cloudflare_metrics_collector_dataset_rows)", + "instant": false, + "range": true, + "legendFormat": "{{dataset}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 27, + "type": "timeseries", + "title": "Pending Flush Buffers", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 50 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_flush_pending_buffers)", + "instant": false, + "range": true, + "legendFormat": "pending", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 28, + "type": "row", + "title": "Estimated Costs (excl. free tier)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 36 + }, + "panels": [] + }, + { + "id": 29, + "type": "stat", + "title": "Workers (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 10M requests + 30M CPU-ms/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 37 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[24h:1m])) / 1e6 * 0.30) + (sum(sum_over_time(max_over_time(cf_workers_invocations_cpu_time_us_sum[1m])[24h:1m])) / 1000 / 1e6 * 0.02)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 30, + "type": "stat", + "title": "D1 (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 25B reads + 50M writes + 5GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 37 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_d1_queries_rows_read[1m])[24h:1m])) / 1e6 * 0.001) + (sum(sum_over_time(max_over_time(cf_d1_queries_rows_written[1m])[24h:1m])) / 1e6 * 1.00) + (last_over_time((sum(cf_d1_storage_database_size_bytes))[6h:]) / 1073741824 * 0.75 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 31, + "type": "stat", + "title": "R2 (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 1M Class A + 10M Class B + 10GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 37 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"PutObject|DeleteObject|CreateMultipartUpload|ListObjects|ListObjectsV2|ListMultipartUploads|ListParts\"}[1m])[24h:1m])) / 1e6 * 4.50) + (sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"GetObject|HeadObject|HeadBucket\"}[1m])[24h:1m])) / 1e6 * 0.36) + (last_over_time((sum(cf_r2_storage_payload_bytes))[6h:]) / 1073741824 * 0.015 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 32, + "type": "stat", + "title": "KV (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 10M reads + 1M writes + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 37 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type=\"read\"}[1m])[24h:1m])) / 1e6 * 0.50) + (sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type!=\"read\"}[1m])[24h:1m])) / 1e6 * 5.00) + (last_over_time((sum(cf_kv_storage_byte_count))[6h:]) / 1073741824 * 0.50 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 33, + "type": "stat", + "title": "Durable Objects (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 1M requests + 400K GB-s + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 37 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[24h:1m])) / 1e6 * 0.15) + (sum(sum_over_time(max_over_time(cf_durable_objects_periodic_duration_ms[1m])[24h:1m])) / 1000 / 1e6 * 12.50) + (last_over_time((sum(cf_durable_objects_storage_stored_bytes))[6h:]) / 1073741824 * 0.20 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 34, + "type": "stat", + "title": "Queues (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 1M operations/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 37 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[24h:1m])) / 1e6 * 0.40", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 35, + "type": "stat", + "title": "Vectorize (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 50M queried dims + 10M stored dims/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_vectorize_queries_queried_vector_dimensions[1m])[24h:1m])) / 1e6 * 0.01) + (last_over_time((sum(cf_vectorize_storage_stored_vector_dimensions))[6h:]) / 1e8 * 0.05 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 36, + "type": "stat", + "title": "Workers AI (24h)", + "description": "Estimated daily cost before free tier.\nFree tier: 10K neurons/day included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_ai_inference_neurons[1m])[24h:1m])) / 1000 * 0.011", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 37, + "type": "stat", + "title": "Total (24h)", + "description": "Estimated total daily cost before free tier deductions.\nBase plan: $5/mo.", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[24h:1m])) / 1e6 * 0.30) + (sum(sum_over_time(max_over_time(cf_workers_invocations_cpu_time_us_sum[1m])[24h:1m])) / 1000 / 1e6 * 0.02)) + ((sum(sum_over_time(max_over_time(cf_d1_queries_rows_read[1m])[24h:1m])) / 1e6 * 0.001) + (sum(sum_over_time(max_over_time(cf_d1_queries_rows_written[1m])[24h:1m])) / 1e6 * 1.00) + (last_over_time((sum(cf_d1_storage_database_size_bytes))[6h:]) / 1073741824 * 0.75 / 30)) + ((sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"PutObject|DeleteObject|CreateMultipartUpload|ListObjects|ListObjectsV2|ListMultipartUploads|ListParts\"}[1m])[24h:1m])) / 1e6 * 4.50) + (sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"GetObject|HeadObject|HeadBucket\"}[1m])[24h:1m])) / 1e6 * 0.36) + (last_over_time((sum(cf_r2_storage_payload_bytes))[6h:]) / 1073741824 * 0.015 / 30)) + ((sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type=\"read\"}[1m])[24h:1m])) / 1e6 * 0.50) + (sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type!=\"read\"}[1m])[24h:1m])) / 1e6 * 5.00) + (last_over_time((sum(cf_kv_storage_byte_count))[6h:]) / 1073741824 * 0.50 / 30)) + ((sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[24h:1m])) / 1e6 * 0.15) + (sum(sum_over_time(max_over_time(cf_durable_objects_periodic_duration_ms[1m])[24h:1m])) / 1000 / 1e6 * 12.50) + (last_over_time((sum(cf_durable_objects_storage_stored_bytes))[6h:]) / 1073741824 * 0.20 / 30)) + (sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[24h:1m])) / 1e6 * 0.40) + ((sum(sum_over_time(max_over_time(cf_vectorize_queries_queried_vector_dimensions[1m])[24h:1m])) / 1e6 * 0.01) + (last_over_time((sum(cf_vectorize_storage_stored_vector_dimensions))[6h:]) / 1e8 * 0.05 / 30)) + (sum(sum_over_time(max_over_time(cf_ai_inference_neurons[1m])[24h:1m])) / 1000 * 0.011)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 38, + "type": "timeseries", + "title": "Estimated Cost per Hour (by product)", + "description": "Hourly cost rate per product, stacked. Excludes free tier allowances and storage costs (which are constant).", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 45 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[1h:1m])) / 1e6 * 0.30) + (sum(sum_over_time(max_over_time(cf_workers_invocations_cpu_time_us_sum[1m])[1h:1m])) / 1000 / 1e6 * 0.02)", + "legendFormat": "Workers", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_d1_queries_rows_read[1m])[1h:1m])) / 1e6 * 0.001) + (sum(sum_over_time(max_over_time(cf_d1_queries_rows_written[1m])[1h:1m])) / 1e6 * 1.00)", + "legendFormat": "D1", + "refId": "B", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"PutObject|DeleteObject|CreateMultipartUpload|ListObjects|ListObjectsV2|ListMultipartUploads|ListParts\"}[1m])[1h:1m])) / 1e6 * 4.50) + (sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"GetObject|HeadObject|HeadBucket\"}[1m])[1h:1m])) / 1e6 * 0.36)", + "legendFormat": "R2", + "refId": "C", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type=\"read\"}[1m])[1h:1m])) / 1e6 * 0.50) + (sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type!=\"read\"}[1m])[1h:1m])) / 1e6 * 5.00)", + "legendFormat": "KV", + "refId": "D", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[1h:1m])) / 1e6 * 0.15) + (sum(sum_over_time(max_over_time(cf_durable_objects_periodic_duration_ms[1m])[1h:1m])) / 1000 / 1e6 * 12.50)", + "legendFormat": "Durable Objects", + "refId": "E", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[1h:1m])) / 1e6 * 0.40", + "legendFormat": "Queues", + "refId": "F", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_vectorize_queries_queried_vector_dimensions[1m])[1h:1m])) / 1e6 * 0.01", + "legendFormat": "Vectorize", + "refId": "G", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_ai_inference_neurons[1m])[1h:1m])) / 1000 * 0.011", + "legendFormat": "Workers AI", + "refId": "H", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Account Overview", + "uid": "cf-account-overview", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/ai.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/ai.json new file mode 100644 index 0000000..e7e58e5 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/ai.json @@ -0,0 +1,1116 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Workers AI Inference", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Requests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_ai_inference_requests[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Neurons (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_ai_inference_neurons[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "timeseries", + "title": "Request Rate by Model", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (model_name) (max_over_time(cf_ai_inference_requests[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{model_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 5, + "type": "timeseries", + "title": "Neurons by Model", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (model_name) (max_over_time(cf_ai_inference_neurons[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{model_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "row", + "title": "AI Gateway", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "Gateway Requests", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (gateway) (max_over_time(cf_ai_gateway_requests_requests[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{gateway}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "Gateway Cache Hit Rate", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (gateway) (max_over_time(cf_ai_gateway_cache_events[1m])) / clamp_min(sum by (gateway) (max_over_time(cf_ai_gateway_requests_requests[1m])), 1)", + "instant": false, + "range": true, + "legendFormat": "{{gateway}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "timeseries", + "title": "Gateway Errors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (gateway) (max_over_time(cf_ai_gateway_errors_errors[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{gateway}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "timeseries", + "title": "Gateway Max Response Rows", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 26 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (gateway) (max_over_time(cf_ai_gateway_size_rows_max[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{gateway}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "row", + "title": "AI Search", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 34 + }, + "panels": [] + }, + { + "id": 12, + "type": "timeseries", + "title": "Search API", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (namespace) (max_over_time(cf_ai_search_api_search_count[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{namespace}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "Ingested Chunks", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 35 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (namespace) (max_over_time(cf_ai_search_ingested_num_chunks[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{namespace}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "row", + "title": "AutoRAG", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "panels": [] + }, + { + "id": 15, + "type": "timeseries", + "title": "Config API", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 44 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (instance) (max_over_time(cf_auto_rag_config_api_search_count[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 16, + "type": "timeseries", + "title": "Engine Running", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 44 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (instance) (max_over_time(cf_auto_rag_engine_running_max[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — AI", + "uid": "cf-ai", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/browser.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/browser.json new file mode 100644 index 0000000..fcb6c5a --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/browser.json @@ -0,0 +1,602 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Browser Rendering", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "API Requests", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (endpoint) (max_over_time(cf_browser_rendering_api_requests[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{endpoint}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Browser Time", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_browser_rendering_browser_time_total_session_duration_ms[1m])", + "instant": false, + "range": true, + "legendFormat": "total ms/s", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "timeseries", + "title": "Sessions (unique)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_browser_rendering_binding_sessions_unique_sessions[1m])", + "instant": false, + "range": true, + "legendFormat": "unique", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 5, + "type": "timeseries", + "title": "Events", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_browser_rendering_events_events[1m])", + "instant": false, + "range": true, + "legendFormat": "events", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "row", + "title": "Browser Isolation", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "Sessions", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_browser_isolation_sessions_sessions[1m])", + "instant": false, + "range": true, + "legendFormat": "sessions", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "User Actions", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_browser_isolation_user_actions_actions[1m])", + "instant": false, + "range": true, + "legendFormat": "actions", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Browser", + "uid": "cf-browser", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/d1.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/d1.json new file mode 100644 index 0000000..115ec38 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/d1.json @@ -0,0 +1,1510 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Queries (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_detail_query_count{database_name=~\"$database_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Errors (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_detail_query_count{database_name=~\"$database_name\",error!=\"\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Rows Read (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_rows_read{database_name=~\"$database_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Rows Written (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_rows_written{database_name=~\"$database_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "Storage Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_d1_storage_database_size_bytes{database_name=~\"$database_name\"}))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "stat", + "title": "Query Duration p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_d1_queries_detail_query_duration_ms_p99{database_name=~\"$database_name\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 8, + "type": "row", + "title": "Query Rate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 9, + "type": "timeseries", + "title": "Queries per Database", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (database_name) (max_over_time(cf_d1_queries_detail_query_count{database_name=~\"$database_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "timeseries", + "title": "Errors per Database", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (database_name) (max_over_time(cf_d1_queries_detail_query_count{database_name=~\"$database_name\",error!=\"\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "row", + "title": "Rows", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 12, + "type": "timeseries", + "title": "Rows Read", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (database_name) (max_over_time(cf_d1_queries_rows_read{database_name=~\"$database_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "Rows Written", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (database_name) (max_over_time(cf_d1_queries_rows_written{database_name=~\"$database_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "row", + "title": "Duration Percentiles", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "panels": [] + }, + { + "id": 15, + "type": "timeseries", + "title": "p50", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (database_name) (max_over_time(cf_d1_queries_detail_query_duration_ms_p50{database_name=~\"$database_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 16, + "type": "timeseries", + "title": "p95", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (database_name) (max_over_time(cf_d1_queries_detail_query_duration_ms_p95{database_name=~\"$database_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 17, + "type": "timeseries", + "title": "p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (database_name) (max_over_time(cf_d1_queries_detail_query_duration_ms_p99{database_name=~\"$database_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 18, + "type": "timeseries", + "title": "Storage Size", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (database_name) (cf_d1_storage_database_size_bytes{database_name=~\"$database_name\"}))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{database_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 19, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "panels": [] + }, + { + "id": 20, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 25B reads + 50M writes + 5GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_d1_queries_rows_read[1m])[24h:1m])) / 1e6 * 0.001) + (sum(sum_over_time(max_over_time(cf_d1_queries_rows_written[1m])[24h:1m])) / 1e6 * 1.00) + (last_over_time((sum(cf_d1_storage_database_size_bytes))[6h:]) / 1073741824 * 0.75 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 21, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 25B reads + 50M writes + 5GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_d1_queries_rows_read[1m])[24h:1m])) / 1e6 * 0.001) + (sum(sum_over_time(max_over_time(cf_d1_queries_rows_written[1m])[24h:1m])) / 1e6 * 1.00) + (last_over_time((sum(cf_d1_storage_database_size_bytes))[6h:]) / 1073741824 * 0.75 / 30)) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 22, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 25B reads + 50M writes + 5GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_rows_read[1m])[1h:1m])) / 1e6 * 0.001", + "legendFormat": "Rows Read", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_d1_queries_rows_written[1m])[1h:1m])) / 1e6 * 1.00", + "legendFormat": "Rows Written", + "refId": "B", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_d1_storage_database_size_bytes))[6h:]) / 1073741824 * 0.75 / 30", + "legendFormat": "Storage (daily)", + "refId": "C", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "database_name", + "label": "Database", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_d1_queries_detail_query_count, database_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_d1_queries_detail_query_count, database_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — D1", + "uid": "cf-d1", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/dns.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/dns.json new file mode 100644 index 0000000..e7ba148 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/dns.json @@ -0,0 +1,554 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Queries", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Queries", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_dns_analytics_queries{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "By Query Type", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (query_type) (max_over_time(cf_dns_analytics_queries{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{query_type}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "timeseries", + "title": "By Response Code", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (response_code) (max_over_time(cf_dns_analytics_queries{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{response_code}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 5, + "type": "timeseries", + "title": "Cache vs Uncached", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_dns_analytics_queries_uncached{zone_name=~\"$zone_name\"}[1m])", + "instant": false, + "range": true, + "legendFormat": "uncached", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_dns_analytics_queries_stale{zone_name=~\"$zone_name\"}[1m])", + "instant": false, + "range": true, + "legendFormat": "stale", + "refId": "B" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "row", + "title": "Processing Time", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "Processing Time (avg)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "avg by (zone_name) (max_over_time(cf_dns_analytics_processing_time_us_avg{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "zone_name", + "label": "Zone", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_dns_analytics_queries, zone_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_dns_analytics_queries, zone_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — DNS", + "uid": "cf-dns", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/durable-objects.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/durable-objects.json new file mode 100644 index 0000000..f6d20dd --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/durable-objects.json @@ -0,0 +1,1472 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Requests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests{script_name=~\"$script_name\",namespace_id=~\"$namespace_id\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Errors (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_durable_objects_invocations_errors{script_name=~\"$script_name\",namespace_id=~\"$namespace_id\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "DO Subrequests (bytes, 24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_durable_objects_subrequests_request_body_size_uncached_bytes{script_name=~\"$script_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_durable_objects_storage_stored_bytes))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "SQL Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_durable_objects_sql_storage_stored_bytes))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "row", + "title": "Invocations", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 8, + "type": "timeseries", + "title": "Request Rate", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_durable_objects_invocations_requests{script_name=~\"$script_name\",namespace_id=~\"$namespace_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "timeseries", + "title": "Errors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_durable_objects_invocations_errors{script_name=~\"$script_name\",namespace_id=~\"$namespace_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "row", + "title": "Periodic (CPU / IO / Connections)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 11, + "type": "timeseries", + "title": "CPU Time", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_durable_objects_periodic_cpu_time_us{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "timeseries", + "title": "Active WS Connections", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_durable_objects_periodic_active_ws_connections_max{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "Rows Read", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 23 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_durable_objects_periodic_rows_read{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "timeseries", + "title": "Rows Written", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 23 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_durable_objects_periodic_rows_written{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 15, + "type": "row", + "title": "Storage", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "panels": [] + }, + { + "id": 16, + "type": "timeseries", + "title": "Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (script_name) (cf_durable_objects_storage_stored_bytes))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 17, + "type": "timeseries", + "title": "SQL Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (script_name) (cf_durable_objects_sql_storage_stored_bytes))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 18, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "panels": [] + }, + { + "id": 19, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 1M requests + 400K GB-s + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[24h:1m])) / 1e6 * 0.15) + (sum(sum_over_time(max_over_time(cf_durable_objects_periodic_duration_ms[1m])[24h:1m])) / 1000 / 1e6 * 12.50) + (last_over_time((sum(cf_durable_objects_storage_stored_bytes))[6h:]) / 1073741824 * 0.20 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 20, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 1M requests + 400K GB-s + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[24h:1m])) / 1e6 * 0.15) + (sum(sum_over_time(max_over_time(cf_durable_objects_periodic_duration_ms[1m])[24h:1m])) / 1000 / 1e6 * 12.50) + (last_over_time((sum(cf_durable_objects_storage_stored_bytes))[6h:]) / 1073741824 * 0.20 / 30)) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 21, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 1M requests + 400K GB-s + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_durable_objects_invocations_requests[1m])[1h:1m])) / 1e6 * 0.15", + "legendFormat": "Requests", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_durable_objects_periodic_duration_ms[1m])[1h:1m])) / 1000 / 1e6 * 12.50", + "legendFormat": "Duration", + "refId": "B", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_durable_objects_storage_stored_bytes))[6h:]) / 1073741824 * 0.20 / 30", + "legendFormat": "Storage (daily)", + "refId": "C", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "script_name", + "label": "Script", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_durable_objects_invocations_requests, script_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_durable_objects_invocations_requests, script_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + }, + { + "name": "namespace_id", + "label": "Namespace", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_durable_objects_invocations_requests, namespace_id)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_durable_objects_invocations_requests, namespace_id)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Durable Objects", + "uid": "cf-durable-objects", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/email.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/email.json new file mode 100644 index 0000000..8d9fcea --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/email.json @@ -0,0 +1,579 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Routing", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Messages by Action", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (action) (max_over_time(cf_email_routing_messages{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{action}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Messages by Status", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (status) (max_over_time(cf_email_routing_messages{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "row", + "title": "Sending", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "panels": [] + }, + { + "id": 5, + "type": "timeseries", + "title": "Sending Events", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (event_type) (max_over_time(cf_email_sending_messages{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{event_type}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "row", + "title": "DMARC", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "DMARC Messages", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (disposition) (max_over_time(cf_dmarc_reports_messages{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{disposition}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "DMARC Pass / SPF / DKIM", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_dmarc_reports_dmarc_pass{zone_name=~\"$zone_name\"}[1m])", + "instant": false, + "range": true, + "legendFormat": "dmarc pass", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_dmarc_reports_spf_pass{zone_name=~\"$zone_name\"}[1m])", + "instant": false, + "range": true, + "legendFormat": "spf pass", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_dmarc_reports_dkim_pass{zone_name=~\"$zone_name\"}[1m])", + "instant": false, + "range": true, + "legendFormat": "dkim pass", + "refId": "C" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "zone_name", + "label": "Zone", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_email_routing_messages, zone_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_email_routing_messages, zone_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Email", + "uid": "cf-email", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/exporter-health.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/exporter-health.json new file mode 100644 index 0000000..9148bc7 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/exporter-health.json @@ -0,0 +1,2032 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Collection (per tick)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 9001, + "type": "stat", + "title": "Datasets / tick", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_cron_summary_datasets)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 2, + "type": "stat", + "title": "Points / tick", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_cron_summary_points)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Errors / tick", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_cron_summary_errors)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 9002, + "type": "stat", + "title": "Cron Exceptions (10m)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cloudflare_metrics_cron_error_count)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 9005, + "type": "stat", + "title": "Pending Flush Buffers", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_flush_pending_buffers)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "row", + "title": "Collector — per dataset", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 5, + "type": "timeseries", + "title": "Rows per Dataset", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ops", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (dataset) (cloudflare_metrics_collector_dataset_rows)", + "instant": false, + "range": true, + "legendFormat": "{{dataset}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "timeseries", + "title": "Points per Dataset", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ops", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (dataset) (cloudflare_metrics_collector_dataset_points)", + "instant": false, + "range": true, + "legendFormat": "{{dataset}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9003, + "type": "timeseries", + "title": "Errors per Dataset", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (dataset, error, error_scope) (max_over_time(cloudflare_metrics_collector_dataset_errors[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{dataset}} ({{error_scope}}: {{error}})", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 7, + "type": "timeseries", + "title": "Duration per Dataset (last value)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (dataset) (cloudflare_metrics_collector_dataset_duration)", + "instant": false, + "range": true, + "legendFormat": "{{dataset}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9008, + "type": "table", + "title": "Recent Error Details", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 22 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "custom.width", + "value": 60 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "error_message" + }, + "properties": [ + { + "id": "custom.width", + "value": 500 + } + ] + } + ] + }, + "options": { + "showHeader": true, + "sortBy": [ + { + "displayName": "Value", + "desc": true + } + ], + "cellHeight": "sm", + "footer": { + "show": false, + "reducer": [ + "sum" + ], + "countRows": false + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (dataset, error, error_scope, error_message) (sum_over_time(cloudflare_metrics_collector_dataset_errors[1h:1m]))", + "instant": true, + "range": false, + "legendFormat": "", + "refId": "A", + "format": "table" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true + }, + "indexByName": {}, + "renameByName": { + "dataset": "Dataset", + "error": "Error Type", + "error_scope": "Scope", + "error_message": "Error Message", + "Value": "Count" + } + } + } + ] + }, + { + "id": 8, + "type": "row", + "title": "GraphQL", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "panels": [] + }, + { + "id": 9006, + "type": "timeseries", + "title": "Requests per Tick", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_graphql_client_requests)", + "instant": false, + "range": true, + "legendFormat": "requests", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9007, + "type": "timeseries", + "title": "Error Responses per Tick", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(max_over_time(cloudflare_metrics_graphql_client_error_responses[1m]))", + "instant": false, + "range": true, + "legendFormat": "error responses", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "row", + "title": "Flush to VictoriaMetrics", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "panels": [] + }, + { + "id": 10, + "type": "timeseries", + "title": "Flush Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_flush_bytes)", + "instant": false, + "range": true, + "legendFormat": "bytes", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "timeseries", + "title": "Flush Duration", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_flush_duration_ms)", + "instant": false, + "range": true, + "legendFormat": "ms", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9004, + "type": "timeseries", + "title": "Flush Errors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(max_over_time(cloudflare_metrics_flush_errors[1m]))", + "instant": false, + "range": true, + "legendFormat": "errors", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "timeseries", + "title": "Pending Buffers", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_flush_pending_buffers)", + "instant": false, + "range": true, + "legendFormat": "buffers", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_flush_pending_bytes)", + "instant": false, + "range": true, + "legendFormat": "bytes", + "refId": "B" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "row", + "title": "Resource Lookups (REST)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 56 + }, + "panels": [] + }, + { + "id": 14, + "type": "timeseries", + "title": "Lookup Calls", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (resource, status) (cloudflare_metrics_resource_lookup_count)", + "instant": false, + "range": true, + "legendFormat": "{{resource}} {{status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 15, + "type": "timeseries", + "title": "Lookup Errors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (resource) (max_over_time(cloudflare_metrics_resource_lookup_errors[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{resource}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 16, + "type": "timeseries", + "title": "Zones Bulk Resolved", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_resource_lookup_requested)", + "instant": false, + "range": true, + "legendFormat": "requested", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_resource_lookup_resolved)", + "instant": false, + "range": true, + "legendFormat": "resolved", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_resource_lookup_failed)", + "instant": false, + "range": true, + "legendFormat": "failed", + "refId": "C" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 17, + "type": "row", + "title": "Worker Isolate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 73 + }, + "panels": [] + }, + { + "id": 18, + "type": "timeseries", + "title": "Isolate Age", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 74 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_isolate_age_seconds)", + "instant": false, + "range": true, + "legendFormat": "age", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 19, + "type": "row", + "title": "Worker HTTP", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 82 + }, + "panels": [] + }, + { + "id": 20, + "type": "timeseries", + "title": "HTTP Responses", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 83 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (path, status) (cloudflare_metrics_http_response_count)", + "instant": false, + "range": true, + "legendFormat": "{{path}} {{status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 21, + "type": "timeseries", + "title": "Handle Duration", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 83 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cloudflare_metrics_handle_request_duration)", + "instant": false, + "range": true, + "legendFormat": "duration", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Exporter Health", + "uid": "cf-exporter-health", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/http.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/http.json new file mode 100644 index 0000000..1140fab --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/http.json @@ -0,0 +1,1422 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Requests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_requests{zone_name=~\"$zone_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Cached Requests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_cached_requests{zone_name=~\"$zone_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Cache Hit Rate", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_http_requests_overview_cached_requests{zone_name=~\"$zone_name\"}) / clamp_min(sum(cf_http_requests_overview_requests{zone_name=~\"$zone_name\"}), 1)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Bytes (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_bytes{zone_name=~\"$zone_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "Cached Bytes (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_cached_bytes{zone_name=~\"$zone_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "stat", + "title": "Visits (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_http_requests_overview_visits{zone_name=~\"$zone_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 8, + "type": "row", + "title": "Traffic", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 9, + "type": "timeseries", + "title": "Requests per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_http_requests_overview_requests{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "timeseries", + "title": "Bytes per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_http_requests_overview_bytes{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "row", + "title": "Cache", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 12, + "type": "timeseries", + "title": "Cache Hit per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_http_requests_overview_cached_requests{zone_name=~\"$zone_name\"}[1m])) / clamp_min(sum by (zone_name) (max_over_time(cf_http_requests_overview_requests{zone_name=~\"$zone_name\"}[1m])), 1)", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "Cache Status Breakdown", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (cache_status) (max_over_time(cf_http_requests_detail_requests{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{cache_status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "row", + "title": "Performance", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "panels": [] + }, + { + "id": 15, + "type": "timeseries", + "title": "Edge TTFB (avg) per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "avg by (zone_name) (max_over_time(cf_http_requests_detail_edge_ttfb_ms_avg{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 16, + "type": "timeseries", + "title": "Origin Response (avg) per Zone", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "avg by (zone_name) (max_over_time(cf_http_requests_detail_origin_response_ms_avg{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 17, + "type": "row", + "title": "Breakdown", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "panels": [] + }, + { + "id": 18, + "type": "timeseries", + "title": "By HTTP Method", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (http_method) (max_over_time(cf_http_requests_detail_requests{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{http_method}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 19, + "type": "timeseries", + "title": "By Response Status", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (edge_response_status) (max_over_time(cf_http_requests_detail_requests{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{edge_response_status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 20, + "type": "timeseries", + "title": "By Country (Top 10)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "topk(10, sum by (client_country) (max_over_time(cf_http_requests_detail_requests{zone_name=~\"$zone_name\"}[1m])))", + "instant": false, + "range": true, + "legendFormat": "{{client_country}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 21, + "type": "timeseries", + "title": "Edge Response Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (zone_name) (max_over_time(cf_http_requests_detail_edge_response_bytes{zone_name=~\"$zone_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{zone_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "zone_name", + "label": "Zone", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_http_requests_overview_requests, zone_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_http_requests_overview_requests, zone_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — HTTP / Zones", + "uid": "cf-http", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/hyperdrive.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/hyperdrive.json new file mode 100644 index 0000000..16e734d --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/hyperdrive.json @@ -0,0 +1,933 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Queries (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_hyperdrive_queries_count{config_id=~\"$config_id\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Bytes Returned (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_hyperdrive_queries_query_bytes{config_id=~\"$config_id\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Pool Size (max)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_hyperdrive_pool_current_pool_size{config_id=~\"$config_id\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Waiting Clients", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_hyperdrive_pool_waiting_clients{config_id=~\"$config_id\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "row", + "title": "Queries", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "Queries", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (config_id) (max_over_time(cf_hyperdrive_queries_count{config_id=~\"$config_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{config_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "Bytes Returned", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (config_id) (max_over_time(cf_hyperdrive_queries_query_bytes{config_id=~\"$config_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{config_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "row", + "title": "Latency (avg per tick)", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 10, + "type": "timeseries", + "title": "Query Latency (avg µs)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (config_id) (max_over_time(cf_hyperdrive_queries_query_latency_us{config_id=~\"$config_id\"}[1m])) / clamp_min(sum by (config_id) (max_over_time(cf_hyperdrive_queries_count{config_id=~\"$config_id\"}[1m])), 1)", + "instant": false, + "range": true, + "legendFormat": "{{config_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "timeseries", + "title": "Connection Latency (avg µs)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (config_id) (max_over_time(cf_hyperdrive_queries_connection_latency_us{config_id=~\"$config_id\"}[1m])) / clamp_min(sum by (config_id) (max_over_time(cf_hyperdrive_queries_count{config_id=~\"$config_id\"}[1m])), 1)", + "instant": false, + "range": true, + "legendFormat": "{{config_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "row", + "title": "Pool", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "panels": [] + }, + { + "id": 13, + "type": "timeseries", + "title": "Pool Size", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (config_id) (max_over_time(cf_hyperdrive_pool_current_pool_size{config_id=~\"$config_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{config_id}} current", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (config_id) (max_over_time(cf_hyperdrive_pool_max_pool_size{config_id=~\"$config_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{config_id}} max", + "refId": "B" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "timeseries", + "title": "Waiting Clients", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (config_id) (max_over_time(cf_hyperdrive_pool_waiting_clients{config_id=~\"$config_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{config_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "config_id", + "label": "Config", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_hyperdrive_queries_count, config_id)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_hyperdrive_queries_count, config_id)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Hyperdrive", + "uid": "cf-hyperdrive", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/kv.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/kv.json new file mode 100644 index 0000000..42d2bb3 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/kv.json @@ -0,0 +1,1075 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Operations (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_kv_operations_requests{namespace_id=~\"$namespace_id\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Key Count", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_kv_storage_key_count{namespace_id=~\"$namespace_id\"}))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_kv_storage_byte_count{namespace_id=~\"$namespace_id\"}))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Read Ops (5m)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_kv_operations_requests{namespace_id=~\"$namespace_id\",action_type=\"read\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "Write Ops (5m)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_kv_operations_requests{namespace_id=~\"$namespace_id\",action_type=\"write\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "row", + "title": "Operations", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 8, + "type": "timeseries", + "title": "Operations per Namespace", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (namespace_id, action_type) (max_over_time(cf_kv_operations_requests{namespace_id=~\"$namespace_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{namespace_id}} {{action_type}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "timeseries", + "title": "Operations by Action", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (action_type) (max_over_time(cf_kv_operations_requests{namespace_id=~\"$namespace_id\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{action_type}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "row", + "title": "Storage", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 11, + "type": "timeseries", + "title": "Key Count", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (namespace_id) (cf_kv_storage_key_count{namespace_id=~\"$namespace_id\"}))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{namespace_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "timeseries", + "title": "Byte Count", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (namespace_id) (cf_kv_storage_byte_count{namespace_id=~\"$namespace_id\"}))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{namespace_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "panels": [] + }, + { + "id": 14, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 10M reads + 1M writes + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type=\"read\"}[1m])[24h:1m])) / 1e6 * 0.50) + (sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type!=\"read\"}[1m])[24h:1m])) / 1e6 * 5.00) + (last_over_time((sum(cf_kv_storage_byte_count))[6h:]) / 1073741824 * 0.50 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 15, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 10M reads + 1M writes + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type=\"read\"}[1m])[24h:1m])) / 1e6 * 0.50) + (sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type!=\"read\"}[1m])[24h:1m])) / 1e6 * 5.00) + (last_over_time((sum(cf_kv_storage_byte_count))[6h:]) / 1073741824 * 0.50 / 30)) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 16, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 10M reads + 1M writes + 1GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type=\"read\"}[1m])[1h:1m])) / 1e6 * 0.50", + "legendFormat": "Reads", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_kv_operations_requests{action_type!=\"read\"}[1m])[1h:1m])) / 1e6 * 5.00", + "legendFormat": "Writes", + "refId": "B", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_kv_storage_byte_count))[6h:]) / 1073741824 * 0.50 / 30", + "legendFormat": "Storage (daily)", + "refId": "C", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "namespace_id", + "label": "Namespace", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_kv_operations_requests, namespace_id)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_kv_operations_requests, namespace_id)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — KV", + "uid": "cf-kv", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/pages-functions.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/pages-functions.json new file mode 100644 index 0000000..aa3cb17 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/pages-functions.json @@ -0,0 +1,730 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Requests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_pages_functions_invocations_requests{script_name=~\"$script_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Errors (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_pages_functions_invocations_errors{script_name=~\"$script_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "CPU p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_pages_functions_invocations_cpu_time_us_p99{script_name=~\"$script_name\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Duration p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_pages_functions_invocations_duration_ms_p99{script_name=~\"$script_name\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "row", + "title": "Traffic", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "Requests per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_pages_functions_invocations_requests{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "Errors per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_pages_functions_invocations_errors{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "row", + "title": "Performance", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 10, + "type": "timeseries", + "title": "CPU p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_pages_functions_invocations_cpu_time_us_p99{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "timeseries", + "title": "Duration p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_pages_functions_invocations_duration_ms_p99{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "script_name", + "label": "Script", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_pages_functions_invocations_requests, script_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_pages_functions_invocations_requests, script_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Pages Functions", + "uid": "cf-pages-functions", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/pipelines.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/pipelines.json new file mode 100644 index 0000000..9183108 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/pipelines.json @@ -0,0 +1,424 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Ingestion", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Ingested Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (pipeline_id) (max_over_time(cf_pipelines_ingestion_ingested_bytes[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{pipeline_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Delivered Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (pipeline_id) (max_over_time(cf_pipelines_delivery_delivered_bytes[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{pipeline_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "row", + "title": "Operators / Sinks", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "panels": [] + }, + { + "id": 5, + "type": "timeseries", + "title": "Operator Bytes In", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (pipeline_id) (max_over_time(cf_pipelines_operator_bytes_in[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{pipeline_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "timeseries", + "title": "Sink Bytes Written", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (pipeline_id) (max_over_time(cf_pipelines_sink_bytes_written[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{pipeline_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Pipelines", + "uid": "cf-pipelines", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/queues.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/queues.json new file mode 100644 index 0000000..7513cc2 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/queues.json @@ -0,0 +1,1153 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Billable Ops (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations{queue_name=~\"$queue_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Bytes (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_bytes{queue_name=~\"$queue_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Backlog Messages", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_queue_backlog_backlog_messages_avg{queue_name=~\"$queue_name\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Backlog Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_queue_backlog_backlog_bytes_avg{queue_name=~\"$queue_name\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "Consumer Concurrency", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "avg(cf_queue_consumer_concurrency_avg{queue_name=~\"$queue_name\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "row", + "title": "Operations", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 8, + "type": "timeseries", + "title": "Billable Ops per Queue", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (queue_name, action_type) (max_over_time(cf_queue_operations_billable_operations{queue_name=~\"$queue_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{queue_name}} {{action_type}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "timeseries", + "title": "Bytes per Queue", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (queue_name) (max_over_time(cf_queue_operations_bytes{queue_name=~\"$queue_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{queue_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "row", + "title": "Backlog", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 11, + "type": "timeseries", + "title": "Backlog Messages", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (queue_name) (max_over_time(cf_queue_backlog_backlog_messages_avg{queue_name=~\"$queue_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{queue_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "timeseries", + "title": "Backlog Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (queue_name) (max_over_time(cf_queue_backlog_backlog_bytes_avg{queue_name=~\"$queue_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{queue_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "row", + "title": "Consumers", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "panels": [] + }, + { + "id": 14, + "type": "timeseries", + "title": "Consumer Concurrency", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "avg by (queue_name) (max_over_time(cf_queue_consumer_concurrency_avg{queue_name=~\"$queue_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{queue_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 15, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "panels": [] + }, + { + "id": 16, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 1M operations/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 33 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[24h:1m])) / 1e6 * 0.40", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 17, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 1M operations/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 33 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[24h:1m])) / 1e6 * 0.40) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 18, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 1M operations/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_queue_operations_billable_operations[1m])[1h:1m])) / 1e6 * 0.40", + "legendFormat": "Operations", + "refId": "A", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "queue_name", + "label": "Queue", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_queue_operations_billable_operations, queue_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_queue_operations_billable_operations, queue_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Queues", + "uid": "cf-queues", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/r2.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/r2.json new file mode 100644 index 0000000..e691796 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/r2.json @@ -0,0 +1,1141 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Operations (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_r2_operations_requests{bucket_name=~\"$bucket_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Bytes Egress (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_r2_operations_response_object_bytes{bucket_name=~\"$bucket_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Object Count", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_r2_storage_object_count{bucket_name=~\"$bucket_name\"}))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_r2_storage_payload_bytes{bucket_name=~\"$bucket_name\"}))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "Metadata Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_r2_storage_metadata_bytes{bucket_name=~\"$bucket_name\"}))[6h:])", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "stat", + "title": "Response Bytes (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_r2_operations_response_bytes{bucket_name=~\"$bucket_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 8, + "type": "row", + "title": "Operations", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 9, + "type": "timeseries", + "title": "Operation per Bucket", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (bucket_name, action_type) (max_over_time(cf_r2_operations_requests{bucket_name=~\"$bucket_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{bucket_name}} {{action_type}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "timeseries", + "title": "Bytes Egress per Bucket", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (bucket_name) (max_over_time(cf_r2_operations_response_object_bytes{bucket_name=~\"$bucket_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{bucket_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "row", + "title": "Storage", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 12, + "type": "timeseries", + "title": "Stored Bytes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (bucket_name, storage_class) (cf_r2_storage_payload_bytes{bucket_name=~\"$bucket_name\"}))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{bucket_name}} {{storage_class}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "Object Count", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum by (bucket_name, storage_class) (cf_r2_storage_object_count{bucket_name=~\"$bucket_name\"}))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{bucket_name}} {{storage_class}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "panels": [] + }, + { + "id": 15, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 1M Class A + 10M Class B + 10GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"PutObject|DeleteObject|CreateMultipartUpload|ListObjects|ListObjectsV2|ListMultipartUploads|ListParts\"}[1m])[24h:1m])) / 1e6 * 4.50) + (sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"GetObject|HeadObject|HeadBucket\"}[1m])[24h:1m])) / 1e6 * 0.36) + (last_over_time((sum(cf_r2_storage_payload_bytes))[6h:]) / 1073741824 * 0.015 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 16, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 1M Class A + 10M Class B + 10GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"PutObject|DeleteObject|CreateMultipartUpload|ListObjects|ListObjectsV2|ListMultipartUploads|ListParts\"}[1m])[24h:1m])) / 1e6 * 4.50) + (sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"GetObject|HeadObject|HeadBucket\"}[1m])[24h:1m])) / 1e6 * 0.36) + (last_over_time((sum(cf_r2_storage_payload_bytes))[6h:]) / 1073741824 * 0.015 / 30)) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 17, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 1M Class A + 10M Class B + 10GB storage/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"PutObject|DeleteObject|CreateMultipartUpload|ListObjects|ListObjectsV2|ListMultipartUploads|ListParts\"}[1m])[1h:1m])) / 1e6 * 4.50", + "legendFormat": "Class A Ops", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_r2_operations_requests{action_type=~\"GetObject|HeadObject|HeadBucket\"}[1m])[1h:1m])) / 1e6 * 0.36", + "legendFormat": "Class B Ops", + "refId": "B", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_r2_storage_payload_bytes))[6h:]) / 1073741824 * 0.015 / 30", + "legendFormat": "Storage (daily)", + "refId": "C", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "bucket_name", + "label": "Bucket", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_r2_operations_requests, bucket_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_r2_operations_requests, bucket_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — R2", + "uid": "cf-r2", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/rum.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/rum.json new file mode 100644 index 0000000..c737b4c --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/rum.json @@ -0,0 +1,335 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "RUM Pageloads", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Pageloads", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (host_name) (max_over_time(cf_rum_pageload_visits[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{host_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Performance Events", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (host_name) (max_over_time(cf_rum_performance_visits[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{host_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "row", + "title": "Web Vitals", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "panels": [] + }, + { + "id": 5, + "type": "timeseries", + "title": "Web Vitals Visits", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (host_name) (max_over_time(cf_rum_web_vitals_visits[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{host_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — RUM", + "uid": "cf-rum", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/stream.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/stream.json new file mode 100644 index 0000000..bd885c3 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/stream.json @@ -0,0 +1,882 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Stream", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Minutes Viewed", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (video_id) (max_over_time(cf_stream_minutes_viewed_minutes_viewed[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{video_id}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "CMCD (ms viewed)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_stream_cmcd_milliseconds_viewed[1m])", + "instant": false, + "range": true, + "legendFormat": "ms/s", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "row", + "title": "Video Events", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "panels": [] + }, + { + "id": 5, + "type": "timeseries", + "title": "Buffer Events", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_video_buffer_events_events[1m])", + "instant": false, + "range": true, + "legendFormat": "events", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "timeseries", + "title": "Playback Events (time viewed)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_video_playback_events_time_viewed_minutes[1m])", + "instant": false, + "range": true, + "legendFormat": "minutes/s", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 7, + "type": "timeseries", + "title": "Quality Events", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_video_quality_events_events[1m])", + "instant": false, + "range": true, + "legendFormat": "events", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "Live Input Events", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_live_input_events_events[1m])", + "instant": false, + "range": true, + "legendFormat": "events", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "row", + "title": "Realtime & Calls", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "panels": [] + }, + { + "id": 10, + "type": "timeseries", + "title": "Realtime Kit Media Minutes", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_realtime_kit_usage_media_minutes[1m])", + "instant": false, + "range": true, + "legendFormat": "minutes/s", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "timeseries", + "title": "Calls Egress", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_calls_usage_egress_bytes[1m])", + "instant": false, + "range": true, + "legendFormat": "bytes/s", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "timeseries", + "title": "Calls TURN Egress", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum (max_over_time(cf_calls_turn_usage_egress_bytes[1m])", + "instant": false, + "range": true, + "legendFormat": "bytes/s", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Stream / Video / Calls", + "uid": "cf-stream", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/vectorize.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/vectorize.json new file mode 100644 index 0000000..c055612 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/vectorize.json @@ -0,0 +1,691 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Operations", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Operations per Index", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (index_name) (max_over_time(cf_vectorize_operations_operations[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{index_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Queries", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (index_name) (max_over_time(cf_vectorize_queries_queries[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{index_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "row", + "title": "Storage / Writes", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "panels": [] + }, + { + "id": 5, + "type": "timeseries", + "title": "Stored Vectors", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((max by (index_name) (cf_vectorize_storage_vector_count))[6h:])", + "instant": false, + "range": true, + "legendFormat": "{{index_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "timeseries", + "title": "Vectors Written", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (index_name) (max_over_time(cf_vectorize_writes_added_vectors[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{index_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 7, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "panels": [] + }, + { + "id": 8, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 50M queried dims + 10M stored dims/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_vectorize_queries_queried_vector_dimensions[1m])[24h:1m])) / 1e6 * 0.01) + (last_over_time((sum(cf_vectorize_storage_stored_vector_dimensions))[6h:]) / 1e8 * 0.05 / 30)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 9, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 50M queried dims + 10M stored dims/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_vectorize_queries_queried_vector_dimensions[1m])[24h:1m])) / 1e6 * 0.01) + (last_over_time((sum(cf_vectorize_storage_stored_vector_dimensions))[6h:]) / 1e8 * 0.05 / 30)) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 10, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 50M queried dims + 10M stored dims/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_vectorize_queries_queried_vector_dimensions[1m])[1h:1m])) / 1e6 * 0.01", + "legendFormat": "Queried Dims", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "last_over_time((sum(cf_vectorize_storage_stored_vector_dimensions))[6h:]) / 1e8 * 0.05 / 30", + "legendFormat": "Stored Dims (daily)", + "refId": "B", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Vectorize", + "uid": "cf-vectorize", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/workers-scheduled.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/workers-scheduled.json new file mode 100644 index 0000000..83a9a12 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/workers-scheduled.json @@ -0,0 +1,758 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Invocations (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_scheduled_invocations{script_name=~\"$script_name\",cron=~\"$cron\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Errors (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_scheduled_invocations{script_name=~\"$script_name\",cron=~\"$cron\",status=\"error\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Error Rate", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_workers_scheduled_invocations{script_name=~\"$script_name\",cron=~\"$cron\",status=\"error\"}) / clamp_min(sum(cf_workers_scheduled_invocations{script_name=~\"$script_name\",cron=~\"$cron\"}), 1)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "CPU Sum (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_scheduled_cpu_time_us_sum{script_name=~\"$script_name\",cron=~\"$cron\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "row", + "title": "Rates", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 7, + "type": "timeseries", + "title": "Invocations per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name, status) (max_over_time(cf_workers_scheduled_invocations{script_name=~\"$script_name\",cron=~\"$cron\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}} {{status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 8, + "type": "timeseries", + "title": "Errors per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_scheduled_invocations{script_name=~\"$script_name\",cron=~\"$cron\",status=\"error\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 9, + "type": "row", + "title": "CPU", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 10, + "type": "timeseries", + "title": "CPU Time Sum", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_scheduled_cpu_time_us_sum{script_name=~\"$script_name\",cron=~\"$cron\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "timeseries", + "title": "CPU Time Max", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_workers_scheduled_cpu_time_us_max{script_name=~\"$script_name\",cron=~\"$cron\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "script_name", + "label": "Script", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_workers_scheduled_invocations, script_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_workers_scheduled_invocations, script_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + }, + { + "name": "cron", + "label": "Cron", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_workers_scheduled_invocations, cron)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_workers_scheduled_invocations, cron)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Workers Scheduled", + "uid": "cf-workers-scheduled", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/workers.json b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/workers.json new file mode 100644 index 0000000..919dde0 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/dashboards/workers.json @@ -0,0 +1,1526 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "id": 1, + "type": "row", + "title": "Overview", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "Requests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_invocations_requests{script_name=~\"$script_name\",status=~\"$status\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 3, + "type": "stat", + "title": "Errors (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_invocations_errors{script_name=~\"$script_name\",status=~\"$status\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 4, + "type": "stat", + "title": "Error Rate", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(cf_workers_invocations_errors{script_name=~\"$script_name\",status=~\"$status\"}) / clamp_min(sum(cf_workers_invocations_requests{script_name=~\"$script_name\",status=~\"$status\"}), 1)", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 5, + "type": "stat", + "title": "Subrequests (24h)", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_subrequests_subrequests{script_name=~\"$script_name\"}[1m])[24h:1m]))", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 6, + "type": "stat", + "title": "CPU Time p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_workers_invocations_cpu_time_us_p99{script_name=~\"$script_name\",status=~\"$status\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 7, + "type": "stat", + "title": "Duration p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "noValue": "0" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max(cf_workers_invocations_duration_ms_p99{script_name=~\"$script_name\",status=~\"$status\"})", + "instant": true, + "range": false, + "legendFormat": "__auto", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + { + "id": 8, + "type": "row", + "title": "Request Rate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "panels": [] + }, + { + "id": 9, + "type": "timeseries", + "title": "Requests per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_invocations_requests{script_name=~\"$script_name\",status=~\"$status\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "timeseries", + "title": "Errors per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_invocations_errors{script_name=~\"$script_name\",status=~\"$status\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "row", + "title": "CPU & Duration", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "panels": [] + }, + { + "id": 12, + "type": "timeseries", + "title": "CPU Time p50", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_workers_invocations_cpu_time_us_p50{script_name=~\"$script_name\",status=~\"$status\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "CPU Time p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "µs", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_workers_invocations_cpu_time_us_p99{script_name=~\"$script_name\",status=~\"$status\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "timeseries", + "title": "Duration p50", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 23 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_workers_invocations_duration_ms_p50{script_name=~\"$script_name\",status=~\"$status\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 15, + "type": "timeseries", + "title": "Duration p99", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 23 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "max by (script_name) (max_over_time(cf_workers_invocations_duration_ms_p99{script_name=~\"$script_name\",status=~\"$status\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 16, + "type": "row", + "title": "Subrequests", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "panels": [] + }, + { + "id": 17, + "type": "timeseries", + "title": "Subrequests per Script", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (script_name) (max_over_time(cf_workers_subrequests_subrequests{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{script_name}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 18, + "type": "timeseries", + "title": "Subrequests by Status", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short", + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "lineWidth": 1, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": true, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum by (http_response_status) (max_over_time(cf_workers_subrequests_subrequests{script_name=~\"$script_name\"}[1m]))", + "instant": false, + "range": true, + "legendFormat": "{{http_response_status}}", + "refId": "A" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 19, + "type": "row", + "title": "Cost Estimate", + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "panels": [] + }, + { + "id": 20, + "type": "stat", + "title": "Estimated Daily Cost", + "description": "Before free tier.\nFree tier: 10M requests + 30M CPU-ms/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "(sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[24h:1m])) / 1e6 * 0.30) + (sum(sum_over_time(max_over_time(cf_workers_invocations_cpu_time_us_sum[1m])[24h:1m])) / 1000 / 1e6 * 0.02)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 21, + "type": "stat", + "title": "Projected Monthly Cost", + "description": "30-day projection based on last 24h usage.\nFree tier: 10M requests + 30M CPU-ms/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "noValue": "$0.00", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "((sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[24h:1m])) / 1e6 * 0.30) + (sum(sum_over_time(max_over_time(cf_workers_invocations_cpu_time_us_sum[1m])[24h:1m])) / 1000 / 1e6 * 0.02)) * 30", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 22, + "type": "timeseries", + "title": "Hourly Cost Breakdown", + "description": "Cost per hour by component, stacked.\nFree tier: 10M requests + 30M CPU-ms/mo included", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_invocations_requests[1m])[1h:1m])) / 1e6 * 0.30", + "legendFormat": "Requests", + "refId": "A", + "instant": false, + "range": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "editorMode": "code", + "expr": "sum(sum_over_time(max_over_time(cf_workers_invocations_cpu_time_us_sum[1m])[1h:1m])) / 1000 / 1e6 * 0.02", + "legendFormat": "CPU Time", + "refId": "B", + "instant": false, + "range": true + } + ] + } + ], + "tags": [ + "cloudflare-workers", + "cloudflare-metrics", + "cloudflare-analytics" + ], + "templating": { + "list": [ + { + "name": "script_name", + "label": "Script", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_workers_invocations_requests, script_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_workers_invocations_requests, script_name)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + }, + { + "name": "status", + "label": "Status", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "36979063-5384-4eb9-8679-565a727cbc13" + }, + "query": { + "query": "label_values(cf_workers_invocations_requests, status)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "hide": 0, + "definition": "label_values(cf_workers_invocations_requests, status)", + "options": [], + "skipUrlSync": false, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Cloudflare — Workers", + "uid": "cf-workers", + "version": 1, + "weekStart": "" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/locals.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/locals.tf new file mode 100644 index 0000000..cf33e57 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/locals.tf @@ -0,0 +1,5 @@ +locals { + resource_stage = var.stage != "" ? "-${var.stage}" : "" + resource_env = "-${var.env}" + resource_suffix = "${local.resource_env}${local.resource_stage}" +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/providers.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/providers.tf new file mode 100644 index 0000000..8b65fe3 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/providers.tf @@ -0,0 +1,17 @@ +provider "cloudflare" { + api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_account +} + +# Separate provider using the bootstrap token (same one the devtools api-keys +# module uses) because the account-scoped token above does not hold the +# "User API Tokens Write" permission required to call `POST /user/tokens`. +# Only the analytics token resource uses this provider. +provider "cloudflare" { + alias = "bootstrap" + api_token = var.cloudflare_api_token +} + +provider "grafana" { + url = var.grafana_url + auth = var.grafana_token +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/remote-state.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/remote-state.tf new file mode 100644 index 0000000..4ce49a6 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/remote-state.tf @@ -0,0 +1,22 @@ +variable "tf_state_postgres_conn_str" { + description = "PostgreSQL connection string for Terraform state" + type = string +} + +data "terraform_remote_state" "api_keys_state" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_api_keys" + } +} + +data "terraform_remote_state" "cloudflare_account" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_account" + } +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/terragrunt.hcl b/deployment/modules/cloudflare/workers/cloudflare-metrics/terragrunt.hcl new file mode 100644 index 0000000..90d3c63 --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/terragrunt.hcl @@ -0,0 +1,30 @@ +terraform { + source = "." + + extra_arguments custom_vars { + commands = get_terraform_commands_that_need_vars() + } +} + +include { + path = find_in_parent_folders("state.hcl") +} + +locals { + env = get_env("TF_VAR_env") + stage = get_env("TF_VAR_stage") + app_name = "cloudflare-metrics" +} + +inputs = { + app_name = local.app_name +} + +remote_state { + backend = "pg" + + config = { + conn_str = get_env("TF_VAR_tf_state_postgres_conn_str") + schema_name = "services_cf_workers_${local.app_name}_${local.env}${local.stage}" + } +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/variables.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/variables.tf new file mode 100644 index 0000000..9269e7e --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/variables.tf @@ -0,0 +1,28 @@ +variable "stage" {} +variable "env" {} +variable "app_name" {} +variable "cloudflare_account_id" {} +variable "dist_dir" {} + +variable "vmetrics_api_token" { + description = "VMetrics API token used by the worker to push metrics to VictoriaMetrics" + type = string + sensitive = true +} + +variable "cloudflare_api_token" { + description = "Bootstrap Cloudflare API token with user-level token write permissions, used only to provision the analytics-read token for this worker. Matches the token used by the devtools api-keys module." + type = string + sensitive = true +} + +variable "grafana_url" { + description = "Grafana instance URL" + type = string +} + +variable "grafana_token" { + description = "Grafana API authentication token" + type = string + sensitive = true +} diff --git a/deployment/modules/cloudflare/workers/cloudflare-metrics/worker.tf b/deployment/modules/cloudflare/workers/cloudflare-metrics/worker.tf new file mode 100644 index 0000000..52dd4fb --- /dev/null +++ b/deployment/modules/cloudflare/workers/cloudflare-metrics/worker.tf @@ -0,0 +1,90 @@ +resource "cloudflare_worker" "worker" { + account_id = var.cloudflare_account_id + name = "${var.app_name}-api${local.resource_suffix}" + logpush = true + observability = { + enabled = true + head_sampling_rate = 1 + logs = { + enabled = true + invocation_logs = true + } + } +} + +resource "terraform_data" "source_hash" { + input = filesha256("${var.dist_dir}/${var.app_name}/index.js") +} + +resource "cloudflare_worker_version" "worker" { + account_id = var.cloudflare_account_id + worker_id = cloudflare_worker.worker.id + usage_model = "standard" + limits = { + cpu_ms = 30000 + } + bindings = [ + { + name = "ENVIRONMENT" + type = "plain_text" + text = var.env + }, + { + name = "CLOUDFLARE_ACCOUNT_ID" + type = "plain_text" + text = var.cloudflare_account_id + }, + { + name = "CLOUDFLARE_API_TOKEN" + type = "secret_text" + text = cloudflare_api_token.analytics_read.value + }, + { + name = "VMETRICS_API_TOKEN" + type = "secret_text" + text = var.vmetrics_api_token + }, + ] + compatibility_date = "2025-09-16" + compatibility_flags = ["nodejs_compat"] + main_module = "index.js" + modules = [ + { + content_file = "${var.dist_dir}/${var.app_name}/index.js" + content_type = "application/javascript+module" + name = "index.js" + } + ] + lifecycle { + replace_triggered_by = [ + terraform_data.source_hash, + cloudflare_api_token.analytics_read, + ] + } +} + +resource "cloudflare_workers_deployment" "worker" { + account_id = var.cloudflare_account_id + script_name = cloudflare_worker.worker.name + strategy = "percentage" + versions = [ + { + percentage = 100 + version_id = cloudflare_worker_version.worker.id + } + ] +} + +resource "cloudflare_workers_cron_trigger" "collect" { + account_id = var.cloudflare_account_id + script_name = cloudflare_worker.worker.name + schedules = [{ cron = "* * * * *" }] + depends_on = [cloudflare_workers_deployment.worker] +} + +# Empty preview_url output keeps the shared CI preview-URL collector happy — +# this worker has no custom domain, but the workflow still calls +# `terragrunt output -raw preview_url` for every module. +output "preview_url" { + value = "" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fbac0b..cba8674 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,12 @@ importers: specifier: ^4.35.0 version: 4.35.0(@cloudflare/workers-types@4.20250909.0) + apps/cloudflare-metrics: + dependencies: + '@influxdata/influxdb-client': + specifier: ^1.34.0 + version: 1.35.0 + apps/github-approval-check: dependencies: '@octokit/app':