From 7e23681b11d7d61d14bd21a19d79ad21be74f23e Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Thu, 16 Oct 2025 15:07:12 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20=F0=9F=8C=90=20=F0=9F=93=8C=20?= =?UTF-8?q?=F0=9F=94=92=20=20(#1396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿšง * ci: apply automated fixes * ci: apply automated fixes (attempt 2/3) * ci: apply automated fixes (attempt 3/3) * โœ๏ธ changelog (#1395) * โœ๏ธ changelog * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * wtf: rewrite * wtf: rewrite * wtf: rewrite * fix: x-proxy * wtf: rewrite * wtf: rewrite * wtf: rewrite * fix: maintenance window minute per day (#1399) * fix: tracker bar (#1398) * fix: tracker bar * ci: apply automated fixes * chore: include post-mortem --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * chore: relaxed status-event messages * blog: rss slack feed (#1397) * blog: rss slack feed * chore: definition and links * fix: links * chore: blog post * fix: empty monitors list (#1401) * fix: more stpg stuff (#1400) * ๐Ÿค— * ๐Ÿ˜‚ * ๐Ÿ˜ฑ * ๐Ÿ˜‚ * ๐Ÿš€ * fix: notifications 404 (#1403) * ๐Ÿบ * ๐Ÿบ * fix: format (#1405) * chore: theme-store package (#1392) * chore: theme-store package * fix: tsc * chore: status-page-configuration * chore: note * chore: note * refactor: configuration token * chore: docs status page beta (#1380) * chore: docs status page beta * chore: cookie note * chore: update docs * fix: small stuff (#1406) * fix: manual validation (#1407) * fix: status page provider validation * fix: manual value * chore: themes subdomain plausible (#1409) * fix: keep regions chart data after update (#1408) * fix: keep regions chart data after update * chore: avoid trigger check if inactive * chore: add footer description * chore: form card info variant (#1411) * fix: redirect monitor create (#1412) * fix: redirect monitor create * fix: resolved only order * chore: docs and changelog * chore: dx status-page env (#1413) * chore: dx status-page env * fix: tsconfig exclude env * Dx improvment (#1414) * ๐Ÿค— * ๐Ÿ˜‚ * fix: og image (#1415) * ๐ŸŒ koyeb (#1404) * ๐ŸŒ * ci: apply automated fixes * ๐ŸŒ * ๐Ÿค— * ๐Ÿค— * ๐ŸŒ koyeb * ci: apply automated fixes * ๐Ÿ”ฅ * ๐ŸŒ * ๐Ÿš€ * ci: apply automated fixes * ๐Ÿ˜‚ * ci: apply automated fixes * ๐Ÿ˜ญ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ci: apply automated fixes * ๐Ÿ˜ฑ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ci: apply automated fixes * hore: form-regions-provider-icon * fix: code label * chore: play checker * fix: tsc * fix: mock * fix: tsc and stuff * fix: dashboard regions * fix: spacing * chore: extract icon cloud provider component --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maximilian Kaske * ๐Ÿ”ฅ (#1417) * chore: delete web dashboard (#1416) * chore: delete web dashboard * chore: delete ping * fix: region column (#1418) * chore: seo (#1419) * chore: seo * chore: seo * chore: seo * chore: sitemap * chore: seo * fix: og * chore: seo * chore: seo * ๐Ÿš‘ * ๐Ÿ› Bug chart missing koyeb (#1420) * ๐Ÿš‘ * ๐Ÿš‘ * chore: improve speed checker (#1422) * chore: improve speed checker * chore: seo content * refactor: icon cloud provider tooltip * chore: response logs cloud provider (#1423) * fix: missing provider info (#1424) * ๐Ÿš€ Add railway (#1421) * ๐ŸŒ * ๐ŸŒ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ci: apply automated fixes * ๐ŸŒ * ๐Ÿš€ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: region code formatter (#1425) * ๐Ÿ˜ฑ * ๐Ÿš€ * ๐Ÿš€ * minor update * wip * ci: apply automated fixes * ๐Ÿค— * wip * wip * ๐Ÿš€ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿงช * add just * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * ๐Ÿ”ฅ * should gen * ๐Ÿ”ฅ * ci: apply automated fixes * feat: crud private regions * feat: apply private regions in monitor form * ๐Ÿ˜‚ * ๐ŸŽ๏ธ * for prod * ๐Ÿ”ฅ * ๐Ÿš€ * chore: region search * fix: table region trucate * new migration * ๐Ÿ”ฅ * ๐Ÿ› * feat: dashboard private locations * ๐Ÿ”ฅ * fix: web build * ๐Ÿ”ฅ * fix: limit regions * chore: upgrade dialog based on limit * wip: docs * fix: docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maximilian Kaske Co-authored-by: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com> --- .../workflows/{checker.yml => go-tests.yml} | 12 +- .github/workflows/publish-checker.yml | 29 +- apps/README.md | 3 + apps/checker/.gitignore | 1 + apps/checker/.golangci.yml | 6 +- apps/checker/.private.air.toml | 47 + apps/checker/.probe.air.toml | 47 + apps/checker/Dockerfile | 2 +- apps/checker/{ => checker}/http.go | 55 +- apps/checker/{ => checker}/http_test.go | 2 +- apps/checker/{ => checker}/tcp.go | 6 +- apps/checker/{ => checker}/tcp_test.go | 4 +- apps/checker/{ => checker}/update.go | 0 apps/checker/cmd/private/main.go | 99 + apps/checker/cmd/{ => server}/main.go | 0 apps/checker/go.mod | 15 +- apps/checker/go.sum | 16 +- apps/checker/handlers/checker.go | 6 +- apps/checker/handlers/ping.go | 2 +- apps/checker/handlers/tcp.go | 10 +- apps/checker/justfile | 20 + apps/checker/pkg/job/http_job.go | 176 ++ apps/checker/pkg/job/http_job_test.go | 52 + apps/checker/pkg/job/job.go | 39 + apps/checker/pkg/job/monitors.go | 45 + apps/checker/pkg/job/tcp_job.go | 114 + apps/checker/pkg/job/tcp_job_test.go | 48 + apps/checker/pkg/otel/otel.go | 2 +- apps/checker/pkg/scheduler/scheduler.go | 291 ++ apps/checker/pkg/scheduler/scheduler_test.go | 111 + apps/checker/pkg/tinybird/client_test.go | 4 +- apps/checker/private-location.Dockerfile | 31 + .../private_location/v1/assertions.pb.go | 420 +++ .../private_location/v1/http_monitor.pb.go | 298 ++ .../v1/private_location.connect.go | 168 ++ .../v1/private_location.pb.go | 549 ++++ .../private_location/v1/tcp_monitor.pb.go | 171 ++ apps/checker/request/request.go | 4 +- .../(dashboard)/monitors/[id]/edit/layout.tsx | 1 + .../app/(dashboard)/monitors/[id]/layout.tsx | 2 +- .../(dashboard)/monitors/[id]/logs/client.tsx | 19 +- .../monitors/[id]/logs/search-params.ts | 6 +- .../monitors/[id]/overview/client.tsx | 32 +- .../monitors/[id]/overview/search-params.ts | 7 +- .../app/(dashboard)/notifications/client.tsx | 3 + .../private-locations/breadcrumb.tsx | 12 + .../(dashboard)/private-locations/client.tsx | 104 + .../(dashboard)/private-locations/layout.tsx | 33 + .../private-locations/nav-actions.tsx | 63 + .../(dashboard)/private-locations/page.tsx | 10 + .../status-pages/[id]/subscribers/page.tsx | 10 +- .../components/chart/chart-area-latency.tsx | 3 +- .../chart/chart-area-timing-phases.tsx | 3 +- .../src/components/chart/chart-bar-uptime.tsx | 3 +- .../components/chart/chart-line-regions.tsx | 47 +- .../controls-search/command-region.tsx | 47 +- .../data-table/audit-logs/columns.tsx | 160 +- .../data-table/audit-logs/wrapper.tsx | 12 +- .../data-table/private-locations/columns.tsx | 61 + .../data-table-row-actions.tsx | 80 + .../data-table/response-logs/columns.tsx | 256 +- .../response-logs/data-table-basics.tsx | 37 +- .../response-logs/data-table-sheet.tsx | 5 +- .../response-logs/regions/columns.tsx | 222 +- .../src/components/dialogs/upgrade.tsx | 20 +- .../forms/monitor/form-scheduling-regions.tsx | 105 +- .../src/components/forms/monitor/update.tsx | 15 +- .../forms/private-location/form.tsx | 211 ++ .../forms/private-location/sheet.tsx | 67 + .../metric/global-uptime/section.tsx | 3 +- .../src/components/nav/app-sidebar.tsx | 6 + .../src/components/ui/input-group.tsx | 170 ++ apps/dashboard/src/data/audit-logs.client.ts | 103 +- apps/dashboard/src/data/metrics.client.ts | 13 +- apps/dashboard/src/data/region-metrics.ts | 8 +- apps/dashboard/src/data/regions.ts | 7 + apps/docs/astro.config.mjs | 4 + .../how-to-create-private-location.mdx | 24 + apps/private-location/.air.toml | 47 + apps/private-location/.gitignore | 1 + apps/private-location/.golangci.yml | 4 + apps/private-location/Dockerfile | 31 + apps/private-location/README.md | 4 + apps/private-location/cmd/server/main.go | 58 + apps/private-location/fly.toml | 42 + apps/private-location/go.mod | 30 + apps/private-location/go.sum | 61 + .../internal/database/database.go | 41 + .../internal/database/models.go | 44 + .../internal/models/assertions.go | 76 + .../internal/server/db_testdata | 465 +++ .../internal/server/ingest_http.go | 92 + .../internal/server/ingest_http_test.go | 129 + .../internal/server/ingest_tcp.go | 82 + .../internal/server/ingest_tcp_test.go | 48 + .../internal/server/monitors.go | 159 + .../internal/server/monitors_test.go | 57 + .../internal/server/routes.go | 60 + .../internal/server/server.go | 41 + .../internal/tinybird/client.go | 69 + .../internal/tinybird/client_test.go | 76 + apps/private-location/justfile | 25 + .../private_location/v1/assertions.pb.go | 420 +++ .../private_location/v1/http_monitor.pb.go | 298 ++ .../v1/private_location.connect.go | 168 ++ .../v1/private_location.pb.go | 549 ++++ .../private_location/v1/tcp_monitor.pb.go | 171 ++ .../play/checker/_components/informations.tsx | 2 +- .../src/components/monitor-charts/utils.tsx | 4 +- .../monitor-dashboard/response-details.tsx | 3 +- .../ping-response-analysis/region-info.tsx | 3 +- .../ping-response-analysis/utils.ts | 6 +- packages/api/src/edge.ts | 2 + packages/api/src/router/monitor.ts | 45 +- packages/api/src/router/privateLocation.ts | 120 + packages/api/src/router/tinybird/index.ts | 20 +- packages/db/drizzle/0048_neat_tempest.sql | 19 + packages/db/drizzle/meta/0048_snapshot.json | 2587 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/index.ts | 1 + packages/db/src/schema/monitors/monitor.ts | 2 + packages/db/src/schema/plan/config.ts | 10 +- packages/db/src/schema/plan/schema.ts | 5 +- packages/db/src/schema/plan/utils.ts | 54 + .../db/src/schema/private_locations/index.ts | 2 + .../private_locations/private_locations.ts | 61 + .../schema/private_locations/validation.ts | 11 + packages/db/src/seed.mts | 43 + packages/proto/buf.gen.yaml | 29 + packages/proto/buf.yaml | 8 + packages/proto/go.mod | 8 + packages/proto/go.sum | 6 + packages/proto/justfile | 13 + packages/proto/package.json | 10 + .../private_location/v1/assertions.proto | 47 + .../private_location/v1/http_monitor.proto | 32 + .../v1/private_location.proto | 61 + .../private_location/v1/tcp_monitor.proto | 18 + packages/regions/index.ts | 25 +- packages/tinybird/src/client.ts | 122 +- pnpm-lock.yaml | 84 + 141 files changed, 11122 insertions(+), 595 deletions(-) rename .github/workflows/{checker.yml => go-tests.yml} (57%) create mode 100644 apps/README.md create mode 100644 apps/checker/.gitignore create mode 100644 apps/checker/.private.air.toml create mode 100644 apps/checker/.probe.air.toml rename apps/checker/{ => checker}/http.go (80%) rename apps/checker/{ => checker}/http_test.go (98%) rename apps/checker/{ => checker}/tcp.go (91%) rename apps/checker/{ => checker}/tcp_test.go (87%) rename apps/checker/{ => checker}/update.go (100%) create mode 100644 apps/checker/cmd/private/main.go rename apps/checker/cmd/{ => server}/main.go (100%) create mode 100644 apps/checker/justfile create mode 100644 apps/checker/pkg/job/http_job.go create mode 100644 apps/checker/pkg/job/http_job_test.go create mode 100644 apps/checker/pkg/job/job.go create mode 100644 apps/checker/pkg/job/monitors.go create mode 100644 apps/checker/pkg/job/tcp_job.go create mode 100644 apps/checker/pkg/job/tcp_job_test.go create mode 100644 apps/checker/pkg/scheduler/scheduler.go create mode 100644 apps/checker/pkg/scheduler/scheduler_test.go create mode 100644 apps/checker/private-location.Dockerfile create mode 100644 apps/checker/proto/private_location/v1/assertions.pb.go create mode 100644 apps/checker/proto/private_location/v1/http_monitor.pb.go create mode 100644 apps/checker/proto/private_location/v1/private_location.connect.go create mode 100644 apps/checker/proto/private_location/v1/private_location.pb.go create mode 100644 apps/checker/proto/private_location/v1/tcp_monitor.pb.go create mode 100644 apps/dashboard/src/app/(dashboard)/private-locations/breadcrumb.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/private-locations/client.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/private-locations/layout.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/private-locations/nav-actions.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/private-locations/page.tsx create mode 100644 apps/dashboard/src/components/data-table/private-locations/columns.tsx create mode 100644 apps/dashboard/src/components/data-table/private-locations/data-table-row-actions.tsx create mode 100644 apps/dashboard/src/components/forms/private-location/form.tsx create mode 100644 apps/dashboard/src/components/forms/private-location/sheet.tsx create mode 100644 apps/dashboard/src/components/ui/input-group.tsx create mode 100644 apps/docs/src/content/docs/tutorial/how-to-create-private-location.mdx create mode 100644 apps/private-location/.air.toml create mode 100644 apps/private-location/.gitignore create mode 100644 apps/private-location/.golangci.yml create mode 100644 apps/private-location/Dockerfile create mode 100644 apps/private-location/README.md create mode 100644 apps/private-location/cmd/server/main.go create mode 100644 apps/private-location/fly.toml create mode 100644 apps/private-location/go.mod create mode 100644 apps/private-location/go.sum create mode 100644 apps/private-location/internal/database/database.go create mode 100644 apps/private-location/internal/database/models.go create mode 100644 apps/private-location/internal/models/assertions.go create mode 100644 apps/private-location/internal/server/db_testdata create mode 100644 apps/private-location/internal/server/ingest_http.go create mode 100644 apps/private-location/internal/server/ingest_http_test.go create mode 100644 apps/private-location/internal/server/ingest_tcp.go create mode 100644 apps/private-location/internal/server/ingest_tcp_test.go create mode 100644 apps/private-location/internal/server/monitors.go create mode 100644 apps/private-location/internal/server/monitors_test.go create mode 100644 apps/private-location/internal/server/routes.go create mode 100644 apps/private-location/internal/server/server.go create mode 100644 apps/private-location/internal/tinybird/client.go create mode 100644 apps/private-location/internal/tinybird/client_test.go create mode 100644 apps/private-location/justfile create mode 100644 apps/private-location/proto/private_location/v1/assertions.pb.go create mode 100644 apps/private-location/proto/private_location/v1/http_monitor.pb.go create mode 100644 apps/private-location/proto/private_location/v1/private_location.connect.go create mode 100644 apps/private-location/proto/private_location/v1/private_location.pb.go create mode 100644 apps/private-location/proto/private_location/v1/tcp_monitor.pb.go create mode 100644 packages/api/src/router/privateLocation.ts create mode 100644 packages/db/drizzle/0048_neat_tempest.sql create mode 100644 packages/db/drizzle/meta/0048_snapshot.json create mode 100644 packages/db/src/schema/private_locations/index.ts create mode 100644 packages/db/src/schema/private_locations/private_locations.ts create mode 100644 packages/db/src/schema/private_locations/validation.ts create mode 100644 packages/proto/buf.gen.yaml create mode 100644 packages/proto/buf.yaml create mode 100644 packages/proto/go.mod create mode 100644 packages/proto/go.sum create mode 100644 packages/proto/justfile create mode 100644 packages/proto/package.json create mode 100644 packages/proto/private_location/v1/assertions.proto create mode 100644 packages/proto/private_location/v1/http_monitor.proto create mode 100644 packages/proto/private_location/v1/private_location.proto create mode 100644 packages/proto/private_location/v1/tcp_monitor.proto diff --git a/.github/workflows/checker.yml b/.github/workflows/go-tests.yml similarity index 57% rename from .github/workflows/checker.yml rename to .github/workflows/go-tests.yml index 49acce97..095fb7ee 100644 --- a/.github/workflows/checker.yml +++ b/.github/workflows/go-tests.yml @@ -1,4 +1,4 @@ -name: Checker CI +name: Go Tests on: push: @@ -9,6 +9,9 @@ on: pull_request: branches: - '**' + paths: + - "apps/checker/**" + - "apps/private-location/**" jobs: ci: @@ -19,7 +22,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '>=1.24.0' - - name: Run test + go-version: '>=1.25.0' + - name: Run test Checker run: go test -timeout 30s -race -count=1 ./... working-directory: apps/checker + - name: Run test Private Location + run: go test -timeout 30s -race -count=1 ./... + working-directory: apps/private-location diff --git a/.github/workflows/publish-checker.yml b/.github/workflows/publish-checker.yml index b5cbe9f6..481fd605 100644 --- a/.github/workflows/publish-checker.yml +++ b/.github/workflows/publish-checker.yml @@ -10,7 +10,7 @@ env: IMAGE_NAME: checker jobs: - build: + build-checker: runs-on: ubuntu-latest # Permissions to use OIDC token authentication permissions: @@ -35,4 +35,29 @@ jobs: tags: ghcr.io/openstatushq/checker:latest platforms: linux/amd64,linux/arm64 push: true - + build-private-location: + runs-on: ubuntu-latest + # Permissions to use OIDC token authentication + permissions: + contents: read + id-token: write + # Allows pushing to the GitHub Container Registry + packages: write + steps: + - uses: actions/checkout@v3 + - uses: depot/setup-action@v1 + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: depot/build-push-action@v1 + with: + file: private-location.Dockerfile + project: 9cknw183m8 + context: apps/checker + tags: ghcr.io/openstatushq/private-location:latest + platforms: linux/amd64,linux/arm64 + push: true diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 00000000..77a79bc6 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,3 @@ +# Apps + +All openstatus apps diff --git a/apps/checker/.gitignore b/apps/checker/.gitignore new file mode 100644 index 00000000..a9a5aecf --- /dev/null +++ b/apps/checker/.gitignore @@ -0,0 +1 @@ +tmp diff --git a/apps/checker/.golangci.yml b/apps/checker/.golangci.yml index 8ce97c53..65109a51 100644 --- a/apps/checker/.golangci.yml +++ b/apps/checker/.golangci.yml @@ -1,4 +1,4 @@ +version: "2" + linters: - enable-all: true - disable-all: false - fast: true + default: fast diff --git a/apps/checker/.private.air.toml b/apps/checker/.private.air.toml new file mode 100644 index 00000000..5126171a --- /dev/null +++ b/apps/checker/.private.air.toml @@ -0,0 +1,47 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/private/main.go" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/apps/checker/.probe.air.toml b/apps/checker/.probe.air.toml new file mode 100644 index 00000000..ba98082f --- /dev/null +++ b/apps/checker/.probe.air.toml @@ -0,0 +1,47 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/server/main.go" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/apps/checker/Dockerfile b/apps/checker/Dockerfile index 04c06236..c069df03 100644 --- a/apps/checker/Dockerfile +++ b/apps/checker/Dockerfile @@ -18,7 +18,7 @@ COPY go.* . RUN go mod download COPY . . -RUN go build -trimpath -ldflags "-s -w" -o checker ./cmd +RUN go build -trimpath -ldflags "-s -w" -o checker ./cmd/server FROM scratch diff --git a/apps/checker/http.go b/apps/checker/checker/http.go similarity index 80% rename from apps/checker/http.go rename to apps/checker/checker/http.go index 49ececce..0f9fdd41 100644 --- a/apps/checker/http.go +++ b/apps/checker/checker/http.go @@ -43,30 +43,42 @@ type Response struct { Timing Timing `json:"timing"` } +// decodeBase64Body decodes a data URL base64 body if needed +func decodeBase64Body(body string) ([]byte, error) { + data := strings.Split(body, ",") + if len(data) == 2 { + return base64.StdEncoding.DecodeString(data[1]) + } + return nil, fmt.Errorf("invalid base64 data url format") +} + // FIXME: This should only return the TCP Timing Data; func Http(ctx context.Context, client *http.Client, inputData request.HttpCheckerRequest) (Response, error) { logger := log.Ctx(ctx).With().Str("monitor", inputData.URL).Logger() - b := []byte(inputData.Body) + var bodyBytes []byte if inputData.Method == http.MethodPost { + contentType := "" for _, header := range inputData.Headers { - if header.Key == "Content-Type" && header.Value == "application/octet-stream" { - // split the body by comma and convert it to bytes it's data url base64 - data := strings.Split(inputData.Body, ",") - if len(data) == 2 { - decoded, err := base64.StdEncoding.DecodeString(data[1]) - if err != nil { - return Response{}, fmt.Errorf("error while decoding base64: %w", err) - } - - b = decoded - - } + if header.Key == "Content-Type" { + contentType = header.Value + break + } + } + if contentType == "application/octet-stream" { + decoded, err := decodeBase64Body(inputData.Body) + if err != nil { + return Response{}, fmt.Errorf("error while decoding base64: %w", err) } + bodyBytes = decoded + } else { + bodyBytes = []byte(inputData.Body) } + } else { + bodyBytes = []byte(inputData.Body) } - req, err := http.NewRequestWithContext(ctx, inputData.Method, inputData.URL, bytes.NewReader(b)) + req, err := http.NewRequestWithContext(ctx, inputData.Method, inputData.URL, bytes.NewReader(bodyBytes)) if err != nil { logger.Error().Err(err).Msg("error while creating req") return Response{}, fmt.Errorf("unable to create req: %w", err) @@ -79,14 +91,9 @@ func Http(ctx context.Context, client *http.Client, inputData request.HttpChecke } // Maybe we should remove the default post to application JSON - if inputData.Method == http.MethodPost { - head := req.Header - _, ok := head["Content-Type"] - - if !ok { - // by default we set the content type to application/json if it's a POST request - req.Header.Set("Content-Type", "application/json") - } + // Default POST Content-Type + if inputData.Method == http.MethodPost && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") } timing := Timing{} @@ -128,7 +135,7 @@ func Http(ctx context.Context, client *http.Client, inputData request.HttpChecke logger.Error().Err(err).Msg("error while pinging") - return Response{}, fmt.Errorf("error with monitorURL %s: %w", inputData.URL, err) + return Response{}, err } defer response.Body.Close() @@ -143,7 +150,7 @@ func Http(ctx context.Context, client *http.Client, inputData request.HttpChecke Timing: timing, Timestamp: start.UTC().UnixMilli(), Error: fmt.Sprintf("Cannot read response body: %s", err.Error()), - }, fmt.Errorf("error with monitorURL %s: %w", inputData.URL, err) + }, err } headers := make(map[string]string) diff --git a/apps/checker/http_test.go b/apps/checker/checker/http_test.go similarity index 98% rename from apps/checker/http_test.go rename to apps/checker/checker/http_test.go index c0dba4f2..13fe3a6b 100644 --- a/apps/checker/http_test.go +++ b/apps/checker/checker/http_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/openstatushq/openstatus/apps/checker" + "github.com/openstatushq/openstatus/apps/checker/checker" "github.com/openstatushq/openstatus/apps/checker/request" ) diff --git a/apps/checker/tcp.go b/apps/checker/checker/tcp.go similarity index 91% rename from apps/checker/tcp.go rename to apps/checker/checker/tcp.go index 5fd99ab3..d1a36cfe 100644 --- a/apps/checker/tcp.go +++ b/apps/checker/checker/tcp.go @@ -31,9 +31,10 @@ type TCPResponse struct { Error uint8 `json:"error,omitempty"` } -func PingTcp(timeout int, url string) (TCPResponseTiming, error) { +func PingTCP(timeout int, url string) (TCPResponseTiming, error) { start := time.Now().UTC().UnixMilli() conn, err := net.DialTimeout("tcp", url, time.Duration(timeout)*time.Second) + stop := time.Now().UTC().UnixMilli() if err != nil { if e := err.(*net.OpError).Timeout(); e { @@ -42,9 +43,8 @@ func PingTcp(timeout int, url string) (TCPResponseTiming, error) { if strings.Contains(err.Error(), "connection refused") { return TCPResponseTiming{}, fmt.Errorf("connection refused") } - return TCPResponseTiming{}, fmt.Errorf("dial error: %v", err) + return TCPResponseTiming{}, fmt.Errorf("dial error: %w", err) } - stop := time.Now().UTC().UnixMilli() defer conn.Close() return TCPResponseTiming{TCPStart: start, TCPDone: stop}, nil diff --git a/apps/checker/tcp_test.go b/apps/checker/checker/tcp_test.go similarity index 87% rename from apps/checker/tcp_test.go rename to apps/checker/checker/tcp_test.go index a2439d70..5f2d135d 100644 --- a/apps/checker/tcp_test.go +++ b/apps/checker/checker/tcp_test.go @@ -3,7 +3,7 @@ package checker_test import ( "testing" - "github.com/openstatushq/openstatus/apps/checker" + "github.com/openstatushq/openstatus/apps/checker/checker" ) func TestPingTcp(t *testing.T) { @@ -22,7 +22,7 @@ func TestPingTcp(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := checker.PingTcp(tt.args.timeout, tt.args.url) + got, err := checker.PingTCP(tt.args.timeout, tt.args.url) if (err != nil) != tt.wantErr { t.Errorf("PingTcp() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/apps/checker/update.go b/apps/checker/checker/update.go similarity index 100% rename from apps/checker/update.go rename to apps/checker/checker/update.go diff --git a/apps/checker/cmd/private/main.go b/apps/checker/cmd/private/main.go new file mode 100644 index 00000000..4d154e1b --- /dev/null +++ b/apps/checker/cmd/private/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "fmt" + "net/http" + + "os" + "os/signal" + "syscall" + "time" + + "connectrpc.com/connect" + "github.com/madflojo/tasks" + "github.com/openstatushq/openstatus/apps/checker/pkg/job" + "github.com/openstatushq/openstatus/apps/checker/pkg/scheduler" + + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +const ( + configRefreshInterval = 1 * time.Minute +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Graceful shutdown on interrupt + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + cancel() + }() + + s := tasks.New() + defer s.Stop() + + apiKey := getEnv("OPENSTATUS_KEY", "my-secret-key") + + monitorManager := scheduler.MonitorManager{ + Client: getClient(apiKey), + JobRunner: job.NewJobRunner(), + Scheduler: s, + } + configTicker := time.NewTicker(configRefreshInterval) + defer configTicker.Stop() + + monitorManager.UpdateMonitors(ctx) + for { + select { + case <-ctx.Done(): + return + case <-configTicker.C: + fmt.Println("fetching monitors") + monitorManager.UpdateMonitors(ctx) + } + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +// UpdateMonitors fetches the latest monitors and starts/stops jobs as needed + +func getClient(apiKey string) v1.PrivateLocationServiceClient { + client := v1.NewPrivateLocationServiceClient( + http.DefaultClient, + "https://openstatus-private-location.fly.dev", + connect.WithHTTPGet(), + connect.WithInterceptors(NewAuthInterceptor(apiKey)), + ) + + return client +} + +func NewAuthInterceptor(token string) connect.UnaryInterceptorFunc { + + interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { + return connect.UnaryFunc(func( + ctx context.Context, + req connect.AnyRequest, + ) (connect.AnyResponse, error) { + if req.Spec().IsClient { + // Send a token with client requests. + req.Header().Set("openstatus-token", token) + } + + return next(ctx, req) + }) + } + return connect.UnaryInterceptorFunc(interceptor) + +} diff --git a/apps/checker/cmd/main.go b/apps/checker/cmd/server/main.go similarity index 100% rename from apps/checker/cmd/main.go rename to apps/checker/cmd/server/main.go diff --git a/apps/checker/go.mod b/apps/checker/go.mod index 3ff61b14..51bb5114 100644 --- a/apps/checker/go.mod +++ b/apps/checker/go.mod @@ -1,9 +1,9 @@ module github.com/openstatushq/openstatus/apps/checker -go 1.24.0 +go 1.25.2 require ( - github.com/cenkalti/backoff/v4 v4.3.0 + connectrpc.com/connect v1.19.1 github.com/gin-gonic/gin v1.9.1 github.com/google/uuid v1.6.0 github.com/rs/zerolog v1.32.0 @@ -13,6 +13,15 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 go.opentelemetry.io/otel/sdk v1.34.0 go.opentelemetry.io/otel/sdk/metric v1.34.0 + google.golang.org/protobuf v1.36.10 +) + +//replace github.com/openstatushq/openstatus/packages/proto => ./../../packages/proto + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/cenkalti/backoff/v5 v5.0.3 + github.com/madflojo/tasks v1.2.1 ) require ( @@ -38,6 +47,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -51,6 +61,5 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/apps/checker/go.sum b/apps/checker/go.sum index fc3e7bc9..872a0b4e 100644 --- a/apps/checker/go.sum +++ b/apps/checker/go.sum @@ -1,9 +1,13 @@ +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -39,8 +43,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -58,6 +62,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/madflojo/tasks v1.2.1 h1:0HMN1RCVf6yDjrlIbthkET1KCB+gxknQG3/SLO+HHj4= +github.com/madflojo/tasks v1.2.1/go.mod h1:/WMv6u3Xb5eyy+aIM76ildaIT166GOxN/jya9oI7dyo= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -77,6 +83,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -133,8 +141,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/apps/checker/handlers/checker.go b/apps/checker/handlers/checker.go index f6801df5..fb472743 100644 --- a/apps/checker/handlers/checker.go +++ b/apps/checker/handlers/checker.go @@ -11,7 +11,7 @@ import ( "github.com/google/uuid" "github.com/rs/zerolog/log" - "github.com/openstatushq/openstatus/apps/checker" + "github.com/openstatushq/openstatus/apps/checker/checker" "github.com/openstatushq/openstatus/apps/checker/pkg/assertions" otelOS "github.com/openstatushq/openstatus/apps/checker/pkg/otel" "github.com/openstatushq/openstatus/apps/checker/request" @@ -65,7 +65,6 @@ func (h Handler) HTTPCheckerHandler(c *gin.Context) { } } - var req request.HttpCheckerRequest if err := c.ShouldBindJSON(&req); err != nil { log.Ctx(ctx).Error().Err(err).Msg("failed to decode checker request") @@ -147,13 +146,10 @@ func (h Handler) HTTPCheckerHandler(c *gin.Context) { switch req.Status { case "active": requestStatus = "success" - break case "error": requestStatus = "error" - break case "degraded": requestStatus = "degraded" - break } data := PingData{ diff --git a/apps/checker/handlers/ping.go b/apps/checker/handlers/ping.go index 1db41834..ddc3032f 100644 --- a/apps/checker/handlers/ping.go +++ b/apps/checker/handlers/ping.go @@ -8,7 +8,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/gin-gonic/gin" - "github.com/openstatushq/openstatus/apps/checker" + "github.com/openstatushq/openstatus/apps/checker/checker" "github.com/openstatushq/openstatus/apps/checker/request" "github.com/rs/zerolog/log" ) diff --git a/apps/checker/handlers/tcp.go b/apps/checker/handlers/tcp.go index 1954d0cf..18d6b2e1 100644 --- a/apps/checker/handlers/tcp.go +++ b/apps/checker/handlers/tcp.go @@ -10,7 +10,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/openstatushq/openstatus/apps/checker" + "github.com/openstatushq/openstatus/apps/checker/checker" otelOS "github.com/openstatushq/openstatus/apps/checker/pkg/otel" "github.com/openstatushq/openstatus/apps/checker/request" "github.com/rs/zerolog/log" @@ -96,7 +96,7 @@ func (h Handler) TCPHandler(c *gin.Context) { } op := func() error { - res, err := checker.PingTcp(int(req.Timeout), req.URI) + res, err := checker.PingTCP(int(req.Timeout), req.URI) if err != nil { return fmt.Errorf("unable to check tcp %s", err) @@ -113,13 +113,11 @@ func (h Handler) TCPHandler(c *gin.Context) { switch req.Status { case "active": requestStatus = "success" - break case "error": requestStatus = "error" - break + case "degraded": requestStatus = "degraded" - break } id, err := uuid.NewV7() @@ -282,7 +280,7 @@ func (h Handler) TCPHandlerRegion(c *gin.Context) { op := func() error { called++ timestamp := time.Now().UTC().UnixMilli() - res, err := checker.PingTcp(int(req.Timeout), req.URI) + res, err := checker.PingTCP(int(req.Timeout), req.URI) if err != nil { return fmt.Errorf("unable to check tcp %s", err) diff --git a/apps/checker/justfile b/apps/checker/justfile new file mode 100644 index 00000000..a23de239 --- /dev/null +++ b/apps/checker/justfile @@ -0,0 +1,20 @@ +build-probe: + go build -o probe ./cmd/server/main.go + +build-private: + go build -o provider ./cmd/private/main.go + +dev-probe: + air -c .probe.air.toml + + +dev-private: + air -c .private.air.toml + +clean: + echo "Cleaning..." + rm -f main + +test: + echo "Running tests..." + go test ./... -v diff --git a/apps/checker/pkg/job/http_job.go b/apps/checker/pkg/job/http_job.go new file mode 100644 index 00000000..a93c013c --- /dev/null +++ b/apps/checker/pkg/job/http_job.go @@ -0,0 +1,176 @@ +package job + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/cenkalti/backoff/v5" + "github.com/google/uuid" + "github.com/openstatushq/openstatus/apps/checker/checker" + "github.com/openstatushq/openstatus/apps/checker/pkg/assertions" + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" + "github.com/openstatushq/openstatus/apps/checker/request" +) + +func (jr jobRunner) HTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*HttpPrivateRegionData, error) { + + retry := monitor.Retry + if retry == 0 { + retry = 3 + } + + requestClient := &http.Client{ + Timeout: time.Duration(monitor.Timeout) * time.Millisecond, + } + defer requestClient.CloseIdleConnections() + + if !monitor.FollowRedirects { + requestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } else { + requestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return http.ErrUseLastResponse + } + return nil + } + } + + var degradedAfter int64 + if monitor.DegradedAt != nil { + degradedAfter = *monitor.DegradedAt + } + + headers := make([]struct { + Key string `json:"key"` + Value string `json:"value"` + }, 0) + if monitor.Headers != nil { + for _, header := range monitor.Headers { + headers = append(headers, struct { + Key string `json:"key"` + Value string `json:"value"` + }{ + Key: header.Key, + Value: header.Value, + }) + } + } + + req := request.HttpCheckerRequest{ + URL: monitor.Url, + MonitorID: monitor.Id, + Method: monitor.Method, + Body: monitor.Body, + Retry: monitor.Retry, + Timeout: monitor.Timeout, + DegradedAfter: degradedAfter, + FollowRedirects: monitor.FollowRedirects, + Headers: headers, + } + + var called int + + op := func() (*HttpPrivateRegionData, error) { + called++ + res, err := checker.Http(ctx, requestClient, req) + if err != nil { + return nil, fmt.Errorf("unable to ping: %w", err) + } + + timingBytes, err := json.Marshal(res.Timing) + if err != nil { + return nil, fmt.Errorf("error while parsing timing data %s: %w", req.URL, err) + } + headersBytes, err := json.Marshal(res.Headers) + if err != nil { + return nil, fmt.Errorf("error while parsing headers %s: %w", req.URL, err) + } + id, err := uuid.NewV7() + if err != nil { + return nil, fmt.Errorf("error while generating uuid: %w", err) + } + + status := statusCode(res.Status) + isSuccessful := status.IsSuccessful() + if len(monitor.HeaderAssertions) > 0 { + headersAsString, err := json.Marshal(res.Headers) + if err != nil { + return nil, fmt.Errorf("error while parsing headers %s: %w", req.URL, err) + } + for _, assertion := range monitor.HeaderAssertions { + assert := assertions.HeaderTarget{ + Comparator: request.StringComparator(assertion.Comparator.String()), + Target: assertion.Target, + Key: assertion.Key, + } + assert.HeaderEvaluate(string(headersAsString)) + } + } + + if len(monitor.StatusCodeAssertions) > 0 { + for _, assertion := range monitor.StatusCodeAssertions { + assert := assertions.StatusTarget{ + Comparator: request.NumberComparator(assertion.Comparator.String()), + Target: assertion.Target, + } + isSuccessful = isSuccessful && assert.StatusEvaluate(int64(res.Status)) + } + } + if len(monitor.BodyAssertions) > 0 { + for _, assertion := range monitor.BodyAssertions { + assert := assertions.StringTargetType{ + Comparator: request.StringComparator(assertion.Comparator.String()), + Target: assertion.Target, + } + isSuccessful = isSuccessful && assert.StringEvaluate(res.Body) + } + } + + requestStatus := "success" + if !isSuccessful { + requestStatus = "error" + } else if req.DegradedAfter > 0 && res.Latency > req.DegradedAfter { + requestStatus = "degraded" + } + + data := HttpPrivateRegionData{ + ID: id.String(), + Latency: res.Latency, + StatusCode: res.Status, + Timestamp: res.Timestamp, + CronTimestamp: res.Timestamp, + URL: req.URL, + // Method: req.Method, + Timing: string(timingBytes), + Headers: string(headersBytes), + Body: "", + RequestStatus: requestStatus, + // Assertions: assertionAsString, + Error: 0, + } + + if isSuccessful { + if req.DegradedAfter != 0 && res.Latency > req.DegradedAfter { + data.Body = res.Body + } + } else { + data.Error = 1 + if called < int(retry) { + return nil, fmt.Errorf("unable to ping: %v with status %v", res, res.Status) + } + } + + return &data, nil + } + + resp, err := backoff.Retry(ctx, op, backoff.WithMaxTries(uint(retry)), backoff.WithBackOff(backoff.NewExponentialBackOff())) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/apps/checker/pkg/job/http_job_test.go b/apps/checker/pkg/job/http_job_test.go new file mode 100644 index 00000000..1ac106f3 --- /dev/null +++ b/apps/checker/pkg/job/http_job_test.go @@ -0,0 +1,52 @@ +package job_test + +import ( + "context" + "testing" + + "github.com/openstatushq/openstatus/apps/checker/pkg/job" + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +// Save original checker.Http for restoration + +func TestHTTPJob_Success(t *testing.T) { + + // Mock checker.Http to simulate success + + monitor := &v1.HTTPMonitor{ + Url: "https://openstat.us", + Method: "GET", + Timeout: 10000, + Retry: 2, + } + + data, err := job.NewJobRunner().HTTPJob(context.Background(), monitor) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if data.RequestStatus != "success" { + t.Errorf("expected RequestStatus 'success', got '%s'", data.RequestStatus) + } + if data.Error != 0 { + t.Errorf("expected Error 0, got %d", data.Error) + } +} + +func TestHTTPJob_Failure(t *testing.T) { + + monitor := &v1.HTTPMonitor{ + Url: "https://localhost:1234", + Method: "GET", + Timeout: 1000, + Retry: 1, + } + + data, err := job.NewJobRunner().HTTPJob(context.Background(), monitor) + if err == nil { + t.Fatalf("expected error, got nil") + } + if data != nil { + t.Errorf("expected data to be nil on error, got %+v", data) + } +} diff --git a/apps/checker/pkg/job/job.go b/apps/checker/pkg/job/job.go new file mode 100644 index 00000000..72154169 --- /dev/null +++ b/apps/checker/pkg/job/job.go @@ -0,0 +1,39 @@ +package job + +import ( + "context" + + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +type statusCode int + +func (s statusCode) IsSuccessful() bool { + return s >= 200 && s < 300 +} + +type HttpPrivateRegionData struct { + ID string `json:"id"` + URL string `json:"url"` + Message string `json:"message,omitempty"` + Timing string `json:"timing,omitempty"` + Headers string `json:"headers,omitempty"` + Body string `json:"body,omitempty"` + RequestStatus string `json:"requestStatus,omitempty"` + Latency int64 `json:"latency"` + CronTimestamp int64 `json:"cronTimestamp"` + Timestamp int64 `json:"timestamp"` + StatusCode int `json:"statusCode,omitempty"` + Error uint8 `json:"error"` +} + +type JobRunner interface { + TCPJob(ctx context.Context, monitor *v1.TCPMonitor) (*TCPPrivateRegionData, error) + HTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*HttpPrivateRegionData, error) +} + +type jobRunner struct{} + +func NewJobRunner() JobRunner { + return &jobRunner{} +} diff --git a/apps/checker/pkg/job/monitors.go b/apps/checker/pkg/job/monitors.go new file mode 100644 index 00000000..647964ae --- /dev/null +++ b/apps/checker/pkg/job/monitors.go @@ -0,0 +1,45 @@ +package job + +type Monitor struct { + ID int `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Periodicity string `json:"periodicity"` + Description string `json:"description"` + Method string `json:"method"` + Regions []string `json:"regions"` + Active bool `json:"active"` + Public bool `json:"public"` + Timeout int `json:"timeout"` + DegradedAfter int `json:"degraded_after,omitempty"` + Body string `json:"body"` + Headers []Header `json:"headers,omitempty"` + Assertions []Assertion `json:"assertions,omitempty"` + Retry int `json:"retry"` + JobType string `json:"jobType"` +} + +type Header struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type Assertion struct { + Type string `json:"type"` + Compare string `json:"compare"` + Key string `json:"key"` + Target any `json:"target"` +} + +type Timing struct { + DnsStart int64 `json:"dnsStart"` + DnsDone int64 `json:"dnsDone"` + ConnectStart int64 `json:"connectStart"` + ConnectDone int64 `json:"connectDone"` + TlsHandshakeStart int64 `json:"tlsHandshakeStart"` + TlsHandshakeDone int64 `json:"tlsHandshakeDone"` + FirstByteStart int64 `json:"firstByteStart"` + FirstByteDone int64 `json:"firstByteDone"` + TransferStart int64 `json:"transferStart"` + TransferDone int64 `json:"transferDone"` +} diff --git a/apps/checker/pkg/job/tcp_job.go b/apps/checker/pkg/job/tcp_job.go new file mode 100644 index 00000000..5c549c19 --- /dev/null +++ b/apps/checker/pkg/job/tcp_job.go @@ -0,0 +1,114 @@ +package job + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cenkalti/backoff/v5" + "github.com/google/uuid" + "github.com/openstatushq/openstatus/apps/checker/checker" + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +// AssertionResult tracks the results of running assertions +type AssertionResult struct { + Type string + Success bool + Message string +} + +// TCPPrivateRegionData represents the result of a TCP monitor check +type TCPPrivateRegionData struct { + ID string `json:"id"` + URI string `json:"uri"` + RequestStatus string `json:"request_status"` + Message string `json:"message"` + Latency int64 `json:"latency"` + Timestamp int64 `json:"timestamp"` + CronTimestamp int64 `json:"cron_timestamp"` + Error int `json:"error"` + Timing string `json:"timing"` +} + +// runAssertions performs all configured assertions for TCP and returns their results + +func (jobRunner) TCPJob(ctx context.Context, monitor *v1.TCPMonitor) (*TCPPrivateRegionData, error) { + retry := monitor.Retry + if retry == 0 { + retry = 3 + } + + var degradedAfter int64 + if monitor.DegradedAt != nil { + degradedAfter = *monitor.DegradedAt + } + + var called int + + op := func() (*TCPPrivateRegionData, error) { + called++ + res, err := checker.PingTCP(int(monitor.Timeout), monitor.Uri) + if err != nil { + if called < int(retry) { + return nil, fmt.Errorf("TCP connection failed: %w", err) + } + // On final attempt, return the error in the result + id, uuidErr := uuid.NewV7() + if uuidErr != nil { + return nil, fmt.Errorf("failed to generate UUID: %w", uuidErr) + } + + return &TCPPrivateRegionData{ + ID: id.String(), + Latency: 0, + Timestamp: res.TCPStart, + CronTimestamp: res.TCPStart, + URI: monitor.Uri, + RequestStatus: "error", + Error: 1, + Message: err.Error(), + }, nil + } + + latency := res.TCPDone - res.TCPStart + + var requestStatus = "active" + + if degradedAfter > 0 && latency > degradedAfter { + requestStatus = "degraded" + } + + id, err := uuid.NewV7() + if err != nil { + return nil, fmt.Errorf("failed to generate UUID: %w", err) + } + timingAsString, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("error while parsing timing data %s: %w", monitor.Uri, err) + } + + data := &TCPPrivateRegionData{ + ID: id.String(), + Latency: latency, + Timestamp: res.TCPStart, + CronTimestamp: res.TCPStart, + URI: monitor.Uri, + RequestStatus: requestStatus, + Error: 0, + Message: fmt.Sprintf("Successfully connected to %s", monitor.Uri), + Timing: string(timingAsString), + } + + return data, nil + } + + resp, err := backoff.Retry(ctx, op, + backoff.WithMaxTries(uint(retry)), + backoff.WithBackOff(backoff.NewExponentialBackOff()), + ) + if err != nil { + return nil, fmt.Errorf("TCP job failed after %d retries: %w", retry, err) + } + return resp, nil +} diff --git a/apps/checker/pkg/job/tcp_job_test.go b/apps/checker/pkg/job/tcp_job_test.go new file mode 100644 index 00000000..f77c4b8e --- /dev/null +++ b/apps/checker/pkg/job/tcp_job_test.go @@ -0,0 +1,48 @@ +package job_test + +import ( + "context" + "testing" + + "github.com/openstatushq/openstatus/apps/checker/pkg/job" + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +func TestTCPJob_Success(t *testing.T) { + + monitor := &v1.TCPMonitor{ + Uri: "openstatus.dev:80", + Timeout: 1, + Retry: 1, + } + data, err := job.NewJobRunner().TCPJob(context.Background(), monitor) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if data.RequestStatus != "active" { + t.Errorf("expected RequestStatus 'active', got '%s'", data.RequestStatus) + } + if data.Error != 0 { + t.Errorf("expected Error 0, got %d", data.Error) + } +} + +func TestTCPJob_Failure(t *testing.T) { + + monitor := &v1.TCPMonitor{ + Uri: "localhost:1234", + Timeout: 1, + Retry: 1, + } + + data, err := job.NewJobRunner().TCPJob(context.Background(), monitor) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if data.RequestStatus != "error" { + t.Errorf("expected RequestStatus 'error', got '%s'", data.RequestStatus) + } + if data.Error != 1 { + t.Errorf("expected Error 1, got %d", data.Error) + } +} diff --git a/apps/checker/pkg/otel/otel.go b/apps/checker/pkg/otel/otel.go index a979f7d8..462bd9e3 100644 --- a/apps/checker/pkg/otel/otel.go +++ b/apps/checker/pkg/otel/otel.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "github.com/openstatushq/openstatus/apps/checker" + "github.com/openstatushq/openstatus/apps/checker/checker" "github.com/openstatushq/openstatus/apps/checker/request" "github.com/rs/zerolog/log" diff --git a/apps/checker/pkg/scheduler/scheduler.go b/apps/checker/pkg/scheduler/scheduler.go new file mode 100644 index 00000000..e64a22e3 --- /dev/null +++ b/apps/checker/pkg/scheduler/scheduler.go @@ -0,0 +1,291 @@ +package scheduler + +import ( + "context" + "log" + "sync" + "time" + + "connectrpc.com/connect" + "github.com/madflojo/tasks" + "github.com/openstatushq/openstatus/apps/checker/pkg/job" + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +const ( + Interval10s = "10s" + Interval30s = "30s" + Interval1m = "1m" + Interval5m = "5m" + Interval10m = "10m" + Interval30m = "30m" + Interval1h = "1h" +) + +type MonitorManager struct { + Client v1.PrivateLocationServiceClient + JobRunner job.JobRunner + Scheduler *tasks.Scheduler + mu sync.Mutex +} + +func (mm *MonitorManager) UpdateMonitors(ctx context.Context) { + res, err := mm.Client.Monitors(ctx, &connect.Request[v1.MonitorsRequest]{}) + if err != nil { + log.Printf("Failed to fetch monitors: %v", err) + return + } + + currentIDs := make(map[string]struct{}) + + // HTTP monitors: start jobs for new monitors + for _, m := range res.Msg.HttpMonitors { + currentIDs[m.Id] = struct{}{} + _, err := mm.Scheduler.Lookup(m.Id) + if err != nil { + interval := time.Duration(intervalToSecond(m.Periodicity)) * time.Second + task := tasks.Task{ + Interval: interval, + RunOnce: false, + RunSingleInstance: true, + // StartAfter: time.Duration(1) * time.Second, + ErrFunc: func(e error) { + log.Printf("An error occurred when executing task %s", e) + }, + FuncWithTaskContext: func(ctx tasks.TaskContext) error { + monitor := m + c := context.Background() + log.Printf("Starting job for monitor %s (%s)", monitor.Id, monitor.Url) + data, err := mm.JobRunner.HTTPJob(c, monitor) + + if err != nil { + log.Printf("Monitor check failed for %s (%s): %v", monitor.Id, monitor.Url, err) + return err + } + resp, ingestErr := mm.Client.IngestHTTP(c, &connect.Request[v1.IngestHTTPRequest]{ + Msg: &v1.IngestHTTPRequest{ + MonitorId: monitor.Id, + Id: data.ID, + Url: monitor.Url, + Message: data.Message, + Latency: data.Latency, + Timing: data.Timing, + Headers: data.Headers, + Body: data.Body, + RequestStatus: data.RequestStatus, + StatusCode: int64(data.StatusCode), + Error: int64(data.Error), + CronTimestamp: data.CronTimestamp, + Timestamp: data.Timestamp, + }, + }) + if ingestErr != nil { + log.Printf("Failed to ingest HTTP result for %s (%s): %v", monitor.Id, monitor.Url, ingestErr) + return ingestErr + } + log.Printf("Monitor check succeeded for %s (%s), ingest response: %v", monitor.Id, monitor.Url, resp) + return nil + }, + } + + err := mm.Scheduler.AddWithID(m.Id, &task) + + if err != nil { + log.Printf("Failed to add HTTP monitor job for %s (%s): %v", m.Id, m.Url, err) + continue + } + log.Printf("Started monitoring job for %s (%s)", m.Id, m.Url) + continue + } + + } + + // // Stop jobs for monitors that no longer exist + // for id := range mm.HttpMonitors { + // if _, stillExists := currentIDs[id]; !stillExists { + // mm.Scheduler.Del(id) + // mm.mu.Lock() + // delete(mm.HttpMonitors, id) + // mm.mu.Unlock() + + // } + // } + + // TCP monitors: start jobs for new monitors + for _, m := range res.Msg.TcpMonitors { + currentIDs[m.Id] = struct{}{} + _, err := mm.Scheduler.Lookup(m.Id) + if err != nil { + + interval := time.Duration(intervalToSecond(m.Periodicity)) * time.Second + task := tasks.Task{ + Interval: interval, + RunOnce: false, + // StartAfter: time.Now().Add(5 * time.Millisecond), + RunSingleInstance: true, + FuncWithTaskContext: func(ctx tasks.TaskContext) error { + + monitor := m + log.Printf("Starting TCP job for monitor %s (%s)", monitor.Id, monitor.Uri) + data, err := mm.JobRunner.TCPJob(ctx.Context, monitor) + if err != nil { + log.Printf("TCP monitor check failed for %s (%s): %v", monitor.Id, monitor.Uri, err) + } + resp, ingestErr := mm.Client.IngestTCP(ctx.Context, &connect.Request[v1.IngestTCPRequest]{ + Msg: &v1.IngestTCPRequest{ + MonitorId: monitor.Id, + Id: data.ID, + Uri: monitor.Uri, + Message: data.Message, + Latency: data.Latency, + RequestStatus: data.RequestStatus, + Error: int64(data.Error), + CronTimestamp: data.CronTimestamp, + Timestamp: data.Timestamp, + }, + }) + if ingestErr != nil { + log.Printf("Failed to ingest TCP result for %s (%s): %v", monitor.Id, monitor.Uri, ingestErr) + return ingestErr + } + log.Printf("TCP monitor check succeeded for %s (%s), ingest response: %v", monitor.Id, monitor.Uri, resp) + + return nil + }, + } + err := mm.Scheduler.AddWithID(m.Id, &task) + + if err != nil { + log.Printf("Failed to add TCP monitor job for %s (%s): %v", m.Id, m.Uri, err) + continue + } + log.Printf("Started TCP monitoring job for %s (%s)", m.Id, m.Uri) + } + } + + for id := range mm.Scheduler.Tasks() { + if _, stillExists := currentIDs[id]; !stillExists { + mm.Scheduler.Del(id) + } + } + // Stop jobs for TCP monitors that no longer exist + // for id := range mm.TcpMonitors { + // if _, stillExists := currentIDs[id]; !stillExists { + // mm.Scheduler.Del(id) + // mm.mu.Lock() + // delete(mm.TcpMonitors, id) + // mm.mu.Unlock() + // } + // } +} + +func intervalToSecond(interval string) int { + switch interval { + case Interval30s: + return 30 + case Interval1m: + return 60 + case Interval5m: + return 300 + case Interval10m: + return 600 + case Interval30m: + return 1800 + case Interval1h: + return 3600 + case Interval10s: + return 10 + default: + return 0 + } +} + +// func (mm *MonitorManager) ScheduleHTTPJob(ctx context.Context, monitor *v1.HTTPMonitor, done chan bool) { +// interval := intervalToSecond(monitor.Periodicity) +// if interval == 0 { +// log.Printf("Invalid interval for monitor %s (%s): %s", monitor.Id, monitor.Url, monitor.Periodicity) +// return +// } + +// ticker := time.NewTicker(time.Duration(1) * time.Second) +// defer ticker.Stop() + +// for { +// select { +// case <-ticker.C: +// log.Printf("Starting job for monitor %s (%s)", monitor.Id, monitor.Url) +// data, err := mm.JobRunner.HTTPJob(ctx, monitor) +// if err != nil { +// log.Printf("Monitor check failed for %s (%s): %v", monitor.Id, monitor.Url, err) +// continue +// } +// resp, ingestErr := mm.Client.IngestHTTP(ctx, &connect.Request[v1.IngestHTTPRequest]{ +// Msg: &v1.IngestHTTPRequest{ +// Id: monitor.Id, +// Url: monitor.Url, +// Message: data.Message, +// Latency: data.Latency, +// Timing: data.Timing, +// Headers: data.Headers, +// Body: data.Body, +// RequestStatus: data.RequestStatus, +// StatusCode: int64(data.StatusCode), +// Error: int64(data.Error), +// CronTimestamp: data.CronTimestamp, +// Timestamp: data.Timestamp, +// }, +// }) +// if ingestErr != nil { +// log.Printf("Failed to ingest HTTP result for %s (%s): %v", monitor.Id, monitor.Url, ingestErr) +// } else { +// log.Printf("Monitor check succeeded for %s (%s), ingest response: %v", monitor.Id, monitor.Url, resp) +// } +// case <-done: +// log.Printf("Shutting down job for monitor %s (%s)", monitor.Id, monitor.Url) +// return +// } +// } +// } + +// func (mm *MonitorManager) ScheduleTCPJob(ctx context.Context, monitor *v1.TCPMonitor, done chan bool) { +// interval := intervalToSecond(monitor.Periodicity) +// if interval == 0 { +// log.Printf("Invalid interval for TCP monitor %s (%s): %s", monitor.Id, monitor.Uri, monitor.Periodicity) +// return +// } + +// ticker := time.NewTicker(time.Duration(1) * time.Second) +// defer ticker.Stop() + +// for { +// select { +// case <-ticker.C: +// log.Printf("Starting TCP job for monitor %s (%s)", monitor.Id, monitor.Uri) +// data, err := mm.JobRunner.TCPJob(ctx, monitor) +// if err != nil { +// log.Printf("TCP monitor check failed for %s (%s): %v", monitor.Id, monitor.Uri, err) +// continue +// } +// resp, ingestErr := mm.Client.IngestTCP(ctx, &connect.Request[v1.IngestTCPRequest]{ +// Msg: &v1.IngestTCPRequest{ +// Id: monitor.Id, +// Uri: monitor.Uri, +// Message: data.Message, +// Latency: data.Latency, +// RequestStatus: data.RequestStatus, +// Error: int64(data.Error), +// CronTimestamp: data.CronTimestamp, +// Timestamp: data.Timestamp, +// }, +// }) +// if ingestErr != nil { +// log.Printf("Failed to ingest TCP result for %s (%s): %v", monitor.Id, monitor.Uri, ingestErr) +// } else { +// log.Printf("TCP monitor check succeeded for %s (%s), ingest response: %v", monitor.Id, monitor.Uri, resp) +// } +// case <-done: +// log.Printf("Shutting down TCP job for monitor %s (%s)", monitor.Id, monitor.Uri) +// return +// } +// } +// } diff --git a/apps/checker/pkg/scheduler/scheduler_test.go b/apps/checker/pkg/scheduler/scheduler_test.go new file mode 100644 index 00000000..be118865 --- /dev/null +++ b/apps/checker/pkg/scheduler/scheduler_test.go @@ -0,0 +1,111 @@ +package scheduler_test + +import ( + "context" + "sync/atomic" + + "sync" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/madflojo/tasks" + "github.com/openstatushq/openstatus/apps/checker/pkg/job" + "github.com/openstatushq/openstatus/apps/checker/pkg/scheduler" + v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1" +) + +// mockJobRunner implements job.JobRunner for testing +type mockJobRunner struct { + HTTPJobCalled atomic.Bool + TCPJobCalled atomic.Bool + mu sync.Mutex +} + +func (m *mockJobRunner) HTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*job.HttpPrivateRegionData, error) { + m.HTTPJobCalled.Store(true) + return &job.HttpPrivateRegionData{}, nil +} +func (m *mockJobRunner) TCPJob(ctx context.Context, monitor *v1.TCPMonitor) (*job.TCPPrivateRegionData, error) { + + m.TCPJobCalled.Store(true) + return &job.TCPPrivateRegionData{}, nil +} + +// mockClient implements v1.PrivateLocationServiceClient for testing +type mockClient struct { + MonitorsFunc func(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) + IngestHTTPFunc func(ctx context.Context, req *connect.Request[v1.IngestHTTPRequest]) (*connect.Response[v1.IngestHTTPResponse], error) + IngestTCPFunc func(ctx context.Context, req *connect.Request[v1.IngestTCPRequest]) (*connect.Response[v1.IngestTCPResponse], error) +} + +func (m *mockClient) Monitors(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) { + return m.MonitorsFunc(ctx, req) +} +func (m *mockClient) IngestHTTP(ctx context.Context, req *connect.Request[v1.IngestHTTPRequest]) (*connect.Response[v1.IngestHTTPResponse], error) { + return m.IngestHTTPFunc(ctx, req) +} +func (m *mockClient) IngestTCP(ctx context.Context, req *connect.Request[v1.IngestTCPRequest]) (*connect.Response[v1.IngestTCPResponse], error) { + return m.IngestTCPFunc(ctx, req) +} + +func TestMonitorManager_StartAndStopJobs_WithJobRunner(t *testing.T) { + ctx := t.Context() + + httpMonitor := &v1.HTTPMonitor{Id: "http1", Url: "http://openstat.us", Periodicity: "10s"} + tcpMonitor := &v1.TCPMonitor{Id: "tcp1", Uri: "openstatus:80", Periodicity: "10s"} + + client := &mockClient{ + MonitorsFunc: func(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) { + return connect.NewResponse(&v1.MonitorsResponse{ + HttpMonitors: []*v1.HTTPMonitor{httpMonitor}, + TcpMonitors: []*v1.TCPMonitor{tcpMonitor}, + }), nil + }, + IngestHTTPFunc: func(ctx context.Context, req *connect.Request[v1.IngestHTTPRequest]) (*connect.Response[v1.IngestHTTPResponse], error) { + return connect.NewResponse(&v1.IngestHTTPResponse{}), nil + }, + IngestTCPFunc: func(ctx context.Context, req *connect.Request[v1.IngestTCPRequest]) (*connect.Response[v1.IngestTCPResponse], error) { + return connect.NewResponse(&v1.IngestTCPResponse{}), nil + }, + } + jobRunner := &mockJobRunner{} + + s := tasks.New() + defer s.Stop() + + mm := &scheduler.MonitorManager{ + + Client: client, + JobRunner: jobRunner, + Scheduler: s, + } + + mm.UpdateMonitors(ctx) + time.Sleep(12 * time.Second) // allow jobs to run + + if !jobRunner.HTTPJobCalled.Load() == true { + t.Errorf("expected HTTPJob to be called") + } + if !jobRunner.TCPJobCalled.Load() == true { + t.Errorf("expected TCPJob to be called") + } + + // Remove monitors and ensure jobs are stopped + client.MonitorsFunc = func(ctx context.Context, req *connect.Request[v1.MonitorsRequest]) (*connect.Response[v1.MonitorsResponse], error) { + return connect.NewResponse(&v1.MonitorsResponse{ + HttpMonitors: []*v1.HTTPMonitor{}, + TcpMonitors: []*v1.TCPMonitor{}, + }), nil + } + mm.UpdateMonitors(ctx) + time.Sleep(1 * time.Second) + + if _, err := mm.Scheduler.Lookup("http1"); err == nil { + t.Errorf("expected HTTP job to be removed") + } + if _, err := mm.Scheduler.Lookup("tcp1"); err == nil { + t.Errorf("expected TCP job to be removed") + } + +} diff --git a/apps/checker/pkg/tinybird/client_test.go b/apps/checker/pkg/tinybird/client_test.go index 806afe18..bc0670be 100644 --- a/apps/checker/pkg/tinybird/client_test.go +++ b/apps/checker/pkg/tinybird/client_test.go @@ -1,7 +1,6 @@ package tinybird_test import ( - "context" "fmt" "net/http" "testing" @@ -27,8 +26,7 @@ func (i *interceptorHTTPClient) GetHTTPClient() *http.Client { func TestSendEvent(t *testing.T) { t.Parallel() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() t.Run("it should return an error if it can not send the event", func(t *testing.T) { interceptor := &interceptorHTTPClient{ diff --git a/apps/checker/private-location.Dockerfile b/apps/checker/private-location.Dockerfile new file mode 100644 index 00000000..aae64e0c --- /dev/null +++ b/apps/checker/private-location.Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.25-alpine as builder + +WORKDIR /go/src/app + +RUN apk add --no-cache tzdata +ENV TZ=UTC + +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 + +COPY go.* . +RUN go mod download + +COPY . . +RUN go build -trimpath -ldflags "-s -w" -o private ./cmd/private + +FROM scratch + +WORKDIR /opt/bin + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo + +COPY --from=builder /go/src/app/private /opt/bin/private + +ENV TZ=UTC +ENV USER=1000 +ENV GIN_MODE=release + +CMD [ "/opt/bin/private" ] diff --git a/apps/checker/proto/private_location/v1/assertions.pb.go b/apps/checker/proto/private_location/v1/assertions.pb.go new file mode 100644 index 00000000..c33032b4 --- /dev/null +++ b/apps/checker/proto/private_location/v1/assertions.pb.go @@ -0,0 +1,420 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: private_location/v1/assertions.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NumberComparator int32 + +const ( + NumberComparator_NUMBER_COMPARATOR_UNSPECIFIED NumberComparator = 0 + NumberComparator_NUMBER_COMPARATOR_EQUAL NumberComparator = 1 + NumberComparator_NUMBER_COMPARATOR_NOT_EQUAL NumberComparator = 2 + NumberComparator_NUMBER_COMPARATOR_GREATER_THAN NumberComparator = 3 + NumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL NumberComparator = 4 + NumberComparator_NUMBER_COMPARATOR_LESS_THAN NumberComparator = 5 + NumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL NumberComparator = 6 +) + +// Enum value maps for NumberComparator. +var ( + NumberComparator_name = map[int32]string{ + 0: "NUMBER_COMPARATOR_UNSPECIFIED", + 1: "NUMBER_COMPARATOR_EQUAL", + 2: "NUMBER_COMPARATOR_NOT_EQUAL", + 3: "NUMBER_COMPARATOR_GREATER_THAN", + 4: "NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL", + 5: "NUMBER_COMPARATOR_LESS_THAN", + 6: "NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL", + } + NumberComparator_value = map[string]int32{ + "NUMBER_COMPARATOR_UNSPECIFIED": 0, + "NUMBER_COMPARATOR_EQUAL": 1, + "NUMBER_COMPARATOR_NOT_EQUAL": 2, + "NUMBER_COMPARATOR_GREATER_THAN": 3, + "NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL": 4, + "NUMBER_COMPARATOR_LESS_THAN": 5, + "NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL": 6, + } +) + +func (x NumberComparator) Enum() *NumberComparator { + p := new(NumberComparator) + *p = x + return p +} + +func (x NumberComparator) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NumberComparator) Descriptor() protoreflect.EnumDescriptor { + return file_private_location_v1_assertions_proto_enumTypes[0].Descriptor() +} + +func (NumberComparator) Type() protoreflect.EnumType { + return &file_private_location_v1_assertions_proto_enumTypes[0] +} + +func (x NumberComparator) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NumberComparator.Descriptor instead. +func (NumberComparator) EnumDescriptor() ([]byte, []int) { + return file_private_location_v1_assertions_proto_rawDescGZIP(), []int{0} +} + +type StringComparator int32 + +const ( + StringComparator_STRING_COMPARATOR_UNSPECIFIED StringComparator = 0 + StringComparator_STRING_COMPARATOR_CONTAINS StringComparator = 1 + StringComparator_STRING_COMPARATOR_NOT_CONTAINS StringComparator = 2 + StringComparator_STRING_COMPARATOR_EQUAL StringComparator = 3 + StringComparator_STRING_COMPARATOR_NOT_EQUAL StringComparator = 4 + StringComparator_STRING_COMPARATOR_EMPTY StringComparator = 5 + StringComparator_STRING_COMPARATOR_NOT_EMPTY StringComparator = 6 + StringComparator_STRING_COMPARATOR_GREATER_THAN StringComparator = 7 + StringComparator_STRING_COMPARATOR_GREATER_THAN_OR_EQUAL StringComparator = 8 + StringComparator_STRING_COMPARATOR_LESS_THAN StringComparator = 9 + StringComparator_STRING_COMPARATOR_LESS_THAN_OR_EQUAL StringComparator = 10 +) + +// Enum value maps for StringComparator. +var ( + StringComparator_name = map[int32]string{ + 0: "STRING_COMPARATOR_UNSPECIFIED", + 1: "STRING_COMPARATOR_CONTAINS", + 2: "STRING_COMPARATOR_NOT_CONTAINS", + 3: "STRING_COMPARATOR_EQUAL", + 4: "STRING_COMPARATOR_NOT_EQUAL", + 5: "STRING_COMPARATOR_EMPTY", + 6: "STRING_COMPARATOR_NOT_EMPTY", + 7: "STRING_COMPARATOR_GREATER_THAN", + 8: "STRING_COMPARATOR_GREATER_THAN_OR_EQUAL", + 9: "STRING_COMPARATOR_LESS_THAN", + 10: "STRING_COMPARATOR_LESS_THAN_OR_EQUAL", + } + StringComparator_value = map[string]int32{ + "STRING_COMPARATOR_UNSPECIFIED": 0, + "STRING_COMPARATOR_CONTAINS": 1, + "STRING_COMPARATOR_NOT_CONTAINS": 2, + "STRING_COMPARATOR_EQUAL": 3, + "STRING_COMPARATOR_NOT_EQUAL": 4, + "STRING_COMPARATOR_EMPTY": 5, + "STRING_COMPARATOR_NOT_EMPTY": 6, + "STRING_COMPARATOR_GREATER_THAN": 7, + "STRING_COMPARATOR_GREATER_THAN_OR_EQUAL": 8, + "STRING_COMPARATOR_LESS_THAN": 9, + "STRING_COMPARATOR_LESS_THAN_OR_EQUAL": 10, + } +) + +func (x StringComparator) Enum() *StringComparator { + p := new(StringComparator) + *p = x + return p +} + +func (x StringComparator) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StringComparator) Descriptor() protoreflect.EnumDescriptor { + return file_private_location_v1_assertions_proto_enumTypes[1].Descriptor() +} + +func (StringComparator) Type() protoreflect.EnumType { + return &file_private_location_v1_assertions_proto_enumTypes[1] +} + +func (x StringComparator) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StringComparator.Descriptor instead. +func (StringComparator) EnumDescriptor() ([]byte, []int) { + return file_private_location_v1_assertions_proto_rawDescGZIP(), []int{1} +} + +type StatusCodeAssertion struct { + state protoimpl.MessageState `protogen:"open.v1"` + Target int64 `protobuf:"varint,1,opt,name=target,proto3" json:"target,omitempty"` + Comparator NumberComparator `protobuf:"varint,2,opt,name=comparator,proto3,enum=private_location.v1.NumberComparator" json:"comparator,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatusCodeAssertion) Reset() { + *x = StatusCodeAssertion{} + mi := &file_private_location_v1_assertions_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatusCodeAssertion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusCodeAssertion) ProtoMessage() {} + +func (x *StatusCodeAssertion) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_assertions_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusCodeAssertion.ProtoReflect.Descriptor instead. +func (*StatusCodeAssertion) Descriptor() ([]byte, []int) { + return file_private_location_v1_assertions_proto_rawDescGZIP(), []int{0} +} + +func (x *StatusCodeAssertion) GetTarget() int64 { + if x != nil { + return x.Target + } + return 0 +} + +func (x *StatusCodeAssertion) GetComparator() NumberComparator { + if x != nil { + return x.Comparator + } + return NumberComparator_NUMBER_COMPARATOR_UNSPECIFIED +} + +type BodyAssertion struct { + state protoimpl.MessageState `protogen:"open.v1"` + Target string `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` + Comparator StringComparator `protobuf:"varint,2,opt,name=comparator,proto3,enum=private_location.v1.StringComparator" json:"comparator,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BodyAssertion) Reset() { + *x = BodyAssertion{} + mi := &file_private_location_v1_assertions_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BodyAssertion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BodyAssertion) ProtoMessage() {} + +func (x *BodyAssertion) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_assertions_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BodyAssertion.ProtoReflect.Descriptor instead. +func (*BodyAssertion) Descriptor() ([]byte, []int) { + return file_private_location_v1_assertions_proto_rawDescGZIP(), []int{1} +} + +func (x *BodyAssertion) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *BodyAssertion) GetComparator() StringComparator { + if x != nil { + return x.Comparator + } + return StringComparator_STRING_COMPARATOR_UNSPECIFIED +} + +type HeaderAssertion struct { + state protoimpl.MessageState `protogen:"open.v1"` + Target string `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` + Comparator StringComparator `protobuf:"varint,2,opt,name=comparator,proto3,enum=private_location.v1.StringComparator" json:"comparator,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeaderAssertion) Reset() { + *x = HeaderAssertion{} + mi := &file_private_location_v1_assertions_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeaderAssertion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeaderAssertion) ProtoMessage() {} + +func (x *HeaderAssertion) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_assertions_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeaderAssertion.ProtoReflect.Descriptor instead. +func (*HeaderAssertion) Descriptor() ([]byte, []int) { + return file_private_location_v1_assertions_proto_rawDescGZIP(), []int{2} +} + +func (x *HeaderAssertion) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *HeaderAssertion) GetComparator() StringComparator { + if x != nil { + return x.Comparator + } + return StringComparator_STRING_COMPARATOR_UNSPECIFIED +} + +func (x *HeaderAssertion) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +var File_private_location_v1_assertions_proto protoreflect.FileDescriptor + +const file_private_location_v1_assertions_proto_rawDesc = "" + + "\n" + + "$private_location/v1/assertions.proto\x12\x13private_location.v1\"t\n" + + "\x13StatusCodeAssertion\x12\x16\n" + + "\x06target\x18\x01 \x01(\x03R\x06target\x12E\n" + + "\n" + + "comparator\x18\x02 \x01(\x0e2%.private_location.v1.NumberComparatorR\n" + + "comparator\"n\n" + + "\rBodyAssertion\x12\x16\n" + + "\x06target\x18\x01 \x01(\tR\x06target\x12E\n" + + "\n" + + "comparator\x18\x02 \x01(\x0e2%.private_location.v1.StringComparatorR\n" + + "comparator\"\x82\x01\n" + + "\x0fHeaderAssertion\x12\x16\n" + + "\x06target\x18\x01 \x01(\tR\x06target\x12E\n" + + "\n" + + "comparator\x18\x02 \x01(\x0e2%.private_location.v1.StringComparatorR\n" + + "comparator\x12\x10\n" + + "\x03key\x18\x03 \x01(\tR\x03key*\x8f\x02\n" + + "\x10NumberComparator\x12!\n" + + "\x1dNUMBER_COMPARATOR_UNSPECIFIED\x10\x00\x12\x1b\n" + + "\x17NUMBER_COMPARATOR_EQUAL\x10\x01\x12\x1f\n" + + "\x1bNUMBER_COMPARATOR_NOT_EQUAL\x10\x02\x12\"\n" + + "\x1eNUMBER_COMPARATOR_GREATER_THAN\x10\x03\x12+\n" + + "'NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\x10\x04\x12\x1f\n" + + "\x1bNUMBER_COMPARATOR_LESS_THAN\x10\x05\x12(\n" + + "$NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\x10\x06*\x91\x03\n" + + "\x10StringComparator\x12!\n" + + "\x1dSTRING_COMPARATOR_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aSTRING_COMPARATOR_CONTAINS\x10\x01\x12\"\n" + + "\x1eSTRING_COMPARATOR_NOT_CONTAINS\x10\x02\x12\x1b\n" + + "\x17STRING_COMPARATOR_EQUAL\x10\x03\x12\x1f\n" + + "\x1bSTRING_COMPARATOR_NOT_EQUAL\x10\x04\x12\x1b\n" + + "\x17STRING_COMPARATOR_EMPTY\x10\x05\x12\x1f\n" + + "\x1bSTRING_COMPARATOR_NOT_EMPTY\x10\x06\x12\"\n" + + "\x1eSTRING_COMPARATOR_GREATER_THAN\x10\a\x12+\n" + + "'STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\x10\b\x12\x1f\n" + + "\x1bSTRING_COMPARATOR_LESS_THAN\x10\t\x12(\n" + + "$STRING_COMPARATOR_LESS_THAN_OR_EQUAL\x10\n" + + "BJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\x06proto3" + +var ( + file_private_location_v1_assertions_proto_rawDescOnce sync.Once + file_private_location_v1_assertions_proto_rawDescData []byte +) + +func file_private_location_v1_assertions_proto_rawDescGZIP() []byte { + file_private_location_v1_assertions_proto_rawDescOnce.Do(func() { + file_private_location_v1_assertions_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_assertions_proto_rawDesc), len(file_private_location_v1_assertions_proto_rawDesc))) + }) + return file_private_location_v1_assertions_proto_rawDescData +} + +var file_private_location_v1_assertions_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_private_location_v1_assertions_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_private_location_v1_assertions_proto_goTypes = []any{ + (NumberComparator)(0), // 0: private_location.v1.NumberComparator + (StringComparator)(0), // 1: private_location.v1.StringComparator + (*StatusCodeAssertion)(nil), // 2: private_location.v1.StatusCodeAssertion + (*BodyAssertion)(nil), // 3: private_location.v1.BodyAssertion + (*HeaderAssertion)(nil), // 4: private_location.v1.HeaderAssertion +} +var file_private_location_v1_assertions_proto_depIdxs = []int32{ + 0, // 0: private_location.v1.StatusCodeAssertion.comparator:type_name -> private_location.v1.NumberComparator + 1, // 1: private_location.v1.BodyAssertion.comparator:type_name -> private_location.v1.StringComparator + 1, // 2: private_location.v1.HeaderAssertion.comparator:type_name -> private_location.v1.StringComparator + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_private_location_v1_assertions_proto_init() } +func file_private_location_v1_assertions_proto_init() { + if File_private_location_v1_assertions_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_assertions_proto_rawDesc), len(file_private_location_v1_assertions_proto_rawDesc)), + NumEnums: 2, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_private_location_v1_assertions_proto_goTypes, + DependencyIndexes: file_private_location_v1_assertions_proto_depIdxs, + EnumInfos: file_private_location_v1_assertions_proto_enumTypes, + MessageInfos: file_private_location_v1_assertions_proto_msgTypes, + }.Build() + File_private_location_v1_assertions_proto = out.File + file_private_location_v1_assertions_proto_goTypes = nil + file_private_location_v1_assertions_proto_depIdxs = nil +} diff --git a/apps/checker/proto/private_location/v1/http_monitor.pb.go b/apps/checker/proto/private_location/v1/http_monitor.pb.go new file mode 100644 index 00000000..866cdc4f --- /dev/null +++ b/apps/checker/proto/private_location/v1/http_monitor.pb.go @@ -0,0 +1,298 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: private_location/v1/http_monitor.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Headers struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Headers) Reset() { + *x = Headers{} + mi := &file_private_location_v1_http_monitor_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Headers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Headers) ProtoMessage() {} + +func (x *Headers) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_http_monitor_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Headers.ProtoReflect.Descriptor instead. +func (*Headers) Descriptor() ([]byte, []int) { + return file_private_location_v1_http_monitor_proto_rawDescGZIP(), []int{0} +} + +func (x *Headers) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Headers) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type HTTPMonitor struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + Periodicity string `protobuf:"bytes,3,opt,name=periodicity,proto3" json:"periodicity,omitempty"` + Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` + Body string `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"` + Timeout int64 `protobuf:"varint,6,opt,name=timeout,proto3" json:"timeout,omitempty"` + DegradedAt *int64 `protobuf:"varint,7,opt,name=degraded_at,json=degradedAt,proto3,oneof" json:"degraded_at,omitempty"` + Retry int64 `protobuf:"varint,8,opt,name=retry,proto3" json:"retry,omitempty"` + FollowRedirects bool `protobuf:"varint,9,opt,name=follow_redirects,json=followRedirects,proto3" json:"follow_redirects,omitempty"` + Headers []*Headers `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty"` + StatusCodeAssertions []*StatusCodeAssertion `protobuf:"bytes,11,rep,name=status_code_assertions,json=statusCodeAssertions,proto3" json:"status_code_assertions,omitempty"` + BodyAssertions []*BodyAssertion `protobuf:"bytes,12,rep,name=body_assertions,json=bodyAssertions,proto3" json:"body_assertions,omitempty"` + HeaderAssertions []*HeaderAssertion `protobuf:"bytes,13,rep,name=header_assertions,json=headerAssertions,proto3" json:"header_assertions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HTTPMonitor) Reset() { + *x = HTTPMonitor{} + mi := &file_private_location_v1_http_monitor_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HTTPMonitor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPMonitor) ProtoMessage() {} + +func (x *HTTPMonitor) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_http_monitor_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPMonitor.ProtoReflect.Descriptor instead. +func (*HTTPMonitor) Descriptor() ([]byte, []int) { + return file_private_location_v1_http_monitor_proto_rawDescGZIP(), []int{1} +} + +func (x *HTTPMonitor) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *HTTPMonitor) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *HTTPMonitor) GetPeriodicity() string { + if x != nil { + return x.Periodicity + } + return "" +} + +func (x *HTTPMonitor) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *HTTPMonitor) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *HTTPMonitor) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *HTTPMonitor) GetDegradedAt() int64 { + if x != nil && x.DegradedAt != nil { + return *x.DegradedAt + } + return 0 +} + +func (x *HTTPMonitor) GetRetry() int64 { + if x != nil { + return x.Retry + } + return 0 +} + +func (x *HTTPMonitor) GetFollowRedirects() bool { + if x != nil { + return x.FollowRedirects + } + return false +} + +func (x *HTTPMonitor) GetHeaders() []*Headers { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HTTPMonitor) GetStatusCodeAssertions() []*StatusCodeAssertion { + if x != nil { + return x.StatusCodeAssertions + } + return nil +} + +func (x *HTTPMonitor) GetBodyAssertions() []*BodyAssertion { + if x != nil { + return x.BodyAssertions + } + return nil +} + +func (x *HTTPMonitor) GetHeaderAssertions() []*HeaderAssertion { + if x != nil { + return x.HeaderAssertions + } + return nil +} + +var File_private_location_v1_http_monitor_proto protoreflect.FileDescriptor + +const file_private_location_v1_http_monitor_proto_rawDesc = "" + + "\n" + + "&private_location/v1/http_monitor.proto\x12\x13private_location.v1\x1a$private_location/v1/assertions.proto\"1\n" + + "\aHeaders\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"\xc6\x04\n" + + "\vHTTPMonitor\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\x12 \n" + + "\vperiodicity\x18\x03 \x01(\tR\vperiodicity\x12\x16\n" + + "\x06method\x18\x04 \x01(\tR\x06method\x12\x12\n" + + "\x04body\x18\x05 \x01(\tR\x04body\x12\x18\n" + + "\atimeout\x18\x06 \x01(\x03R\atimeout\x12$\n" + + "\vdegraded_at\x18\a \x01(\x03H\x00R\n" + + "degradedAt\x88\x01\x01\x12\x14\n" + + "\x05retry\x18\b \x01(\x03R\x05retry\x12)\n" + + "\x10follow_redirects\x18\t \x01(\bR\x0ffollowRedirects\x126\n" + + "\aheaders\x18\n" + + " \x03(\v2\x1c.private_location.v1.HeadersR\aheaders\x12^\n" + + "\x16status_code_assertions\x18\v \x03(\v2(.private_location.v1.StatusCodeAssertionR\x14statusCodeAssertions\x12K\n" + + "\x0fbody_assertions\x18\f \x03(\v2\".private_location.v1.BodyAssertionR\x0ebodyAssertions\x12Q\n" + + "\x11header_assertions\x18\r \x03(\v2$.private_location.v1.HeaderAssertionR\x10headerAssertionsB\x0e\n" + + "\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\x06proto3" + +var ( + file_private_location_v1_http_monitor_proto_rawDescOnce sync.Once + file_private_location_v1_http_monitor_proto_rawDescData []byte +) + +func file_private_location_v1_http_monitor_proto_rawDescGZIP() []byte { + file_private_location_v1_http_monitor_proto_rawDescOnce.Do(func() { + file_private_location_v1_http_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_http_monitor_proto_rawDesc), len(file_private_location_v1_http_monitor_proto_rawDesc))) + }) + return file_private_location_v1_http_monitor_proto_rawDescData +} + +var file_private_location_v1_http_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_private_location_v1_http_monitor_proto_goTypes = []any{ + (*Headers)(nil), // 0: private_location.v1.Headers + (*HTTPMonitor)(nil), // 1: private_location.v1.HTTPMonitor + (*StatusCodeAssertion)(nil), // 2: private_location.v1.StatusCodeAssertion + (*BodyAssertion)(nil), // 3: private_location.v1.BodyAssertion + (*HeaderAssertion)(nil), // 4: private_location.v1.HeaderAssertion +} +var file_private_location_v1_http_monitor_proto_depIdxs = []int32{ + 0, // 0: private_location.v1.HTTPMonitor.headers:type_name -> private_location.v1.Headers + 2, // 1: private_location.v1.HTTPMonitor.status_code_assertions:type_name -> private_location.v1.StatusCodeAssertion + 3, // 2: private_location.v1.HTTPMonitor.body_assertions:type_name -> private_location.v1.BodyAssertion + 4, // 3: private_location.v1.HTTPMonitor.header_assertions:type_name -> private_location.v1.HeaderAssertion + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_private_location_v1_http_monitor_proto_init() } +func file_private_location_v1_http_monitor_proto_init() { + if File_private_location_v1_http_monitor_proto != nil { + return + } + file_private_location_v1_assertions_proto_init() + file_private_location_v1_http_monitor_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_http_monitor_proto_rawDesc), len(file_private_location_v1_http_monitor_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_private_location_v1_http_monitor_proto_goTypes, + DependencyIndexes: file_private_location_v1_http_monitor_proto_depIdxs, + MessageInfos: file_private_location_v1_http_monitor_proto_msgTypes, + }.Build() + File_private_location_v1_http_monitor_proto = out.File + file_private_location_v1_http_monitor_proto_goTypes = nil + file_private_location_v1_http_monitor_proto_depIdxs = nil +} diff --git a/apps/checker/proto/private_location/v1/private_location.connect.go b/apps/checker/proto/private_location/v1/private_location.connect.go new file mode 100644 index 00000000..25b3e700 --- /dev/null +++ b/apps/checker/proto/private_location/v1/private_location.connect.go @@ -0,0 +1,168 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: private_location/v1/private_location.proto + +package v1 + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // PrivateLocationServiceName is the fully-qualified name of the PrivateLocationService service. + PrivateLocationServiceName = "private_location.v1.PrivateLocationService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // PrivateLocationServiceMonitorsProcedure is the fully-qualified name of the + // PrivateLocationService's Monitors RPC. + PrivateLocationServiceMonitorsProcedure = "/private_location.v1.PrivateLocationService/Monitors" + // PrivateLocationServiceIngestTCPProcedure is the fully-qualified name of the + // PrivateLocationService's IngestTCP RPC. + PrivateLocationServiceIngestTCPProcedure = "/private_location.v1.PrivateLocationService/IngestTCP" + // PrivateLocationServiceIngestHTTPProcedure is the fully-qualified name of the + // PrivateLocationService's IngestHTTP RPC. + PrivateLocationServiceIngestHTTPProcedure = "/private_location.v1.PrivateLocationService/IngestHTTP" +) + +// PrivateLocationServiceClient is a client for the private_location.v1.PrivateLocationService +// service. +type PrivateLocationServiceClient interface { + Monitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) + IngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) + IngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) +} + +// NewPrivateLocationServiceClient constructs a client for the +// private_location.v1.PrivateLocationService service. By default, it uses the Connect protocol with +// the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed requests. To use +// the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewPrivateLocationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PrivateLocationServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + privateLocationServiceMethods := File_private_location_v1_private_location_proto.Services().ByName("PrivateLocationService").Methods() + return &privateLocationServiceClient{ + monitors: connect.NewClient[MonitorsRequest, MonitorsResponse]( + httpClient, + baseURL+PrivateLocationServiceMonitorsProcedure, + connect.WithSchema(privateLocationServiceMethods.ByName("Monitors")), + connect.WithClientOptions(opts...), + ), + ingestTCP: connect.NewClient[IngestTCPRequest, IngestTCPResponse]( + httpClient, + baseURL+PrivateLocationServiceIngestTCPProcedure, + connect.WithSchema(privateLocationServiceMethods.ByName("IngestTCP")), + connect.WithClientOptions(opts...), + ), + ingestHTTP: connect.NewClient[IngestHTTPRequest, IngestHTTPResponse]( + httpClient, + baseURL+PrivateLocationServiceIngestHTTPProcedure, + connect.WithSchema(privateLocationServiceMethods.ByName("IngestHTTP")), + connect.WithClientOptions(opts...), + ), + } +} + +// privateLocationServiceClient implements PrivateLocationServiceClient. +type privateLocationServiceClient struct { + monitors *connect.Client[MonitorsRequest, MonitorsResponse] + ingestTCP *connect.Client[IngestTCPRequest, IngestTCPResponse] + ingestHTTP *connect.Client[IngestHTTPRequest, IngestHTTPResponse] +} + +// Monitors calls private_location.v1.PrivateLocationService.Monitors. +func (c *privateLocationServiceClient) Monitors(ctx context.Context, req *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) { + return c.monitors.CallUnary(ctx, req) +} + +// IngestTCP calls private_location.v1.PrivateLocationService.IngestTCP. +func (c *privateLocationServiceClient) IngestTCP(ctx context.Context, req *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) { + return c.ingestTCP.CallUnary(ctx, req) +} + +// IngestHTTP calls private_location.v1.PrivateLocationService.IngestHTTP. +func (c *privateLocationServiceClient) IngestHTTP(ctx context.Context, req *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) { + return c.ingestHTTP.CallUnary(ctx, req) +} + +// PrivateLocationServiceHandler is an implementation of the +// private_location.v1.PrivateLocationService service. +type PrivateLocationServiceHandler interface { + Monitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) + IngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) + IngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) +} + +// NewPrivateLocationServiceHandler builds an HTTP handler from the service implementation. It +// returns the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewPrivateLocationServiceHandler(svc PrivateLocationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + privateLocationServiceMethods := File_private_location_v1_private_location_proto.Services().ByName("PrivateLocationService").Methods() + privateLocationServiceMonitorsHandler := connect.NewUnaryHandler( + PrivateLocationServiceMonitorsProcedure, + svc.Monitors, + connect.WithSchema(privateLocationServiceMethods.ByName("Monitors")), + connect.WithHandlerOptions(opts...), + ) + privateLocationServiceIngestTCPHandler := connect.NewUnaryHandler( + PrivateLocationServiceIngestTCPProcedure, + svc.IngestTCP, + connect.WithSchema(privateLocationServiceMethods.ByName("IngestTCP")), + connect.WithHandlerOptions(opts...), + ) + privateLocationServiceIngestHTTPHandler := connect.NewUnaryHandler( + PrivateLocationServiceIngestHTTPProcedure, + svc.IngestHTTP, + connect.WithSchema(privateLocationServiceMethods.ByName("IngestHTTP")), + connect.WithHandlerOptions(opts...), + ) + return "/private_location.v1.PrivateLocationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case PrivateLocationServiceMonitorsProcedure: + privateLocationServiceMonitorsHandler.ServeHTTP(w, r) + case PrivateLocationServiceIngestTCPProcedure: + privateLocationServiceIngestTCPHandler.ServeHTTP(w, r) + case PrivateLocationServiceIngestHTTPProcedure: + privateLocationServiceIngestHTTPHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedPrivateLocationServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedPrivateLocationServiceHandler struct{} + +func (UnimplementedPrivateLocationServiceHandler) Monitors(context.Context, *connect.Request[MonitorsRequest]) (*connect.Response[MonitorsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("private_location.v1.PrivateLocationService.Monitors is not implemented")) +} + +func (UnimplementedPrivateLocationServiceHandler) IngestTCP(context.Context, *connect.Request[IngestTCPRequest]) (*connect.Response[IngestTCPResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("private_location.v1.PrivateLocationService.IngestTCP is not implemented")) +} + +func (UnimplementedPrivateLocationServiceHandler) IngestHTTP(context.Context, *connect.Request[IngestHTTPRequest]) (*connect.Response[IngestHTTPResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("private_location.v1.PrivateLocationService.IngestHTTP is not implemented")) +} diff --git a/apps/checker/proto/private_location/v1/private_location.pb.go b/apps/checker/proto/private_location/v1/private_location.pb.go new file mode 100644 index 00000000..af801e36 --- /dev/null +++ b/apps/checker/proto/private_location/v1/private_location.pb.go @@ -0,0 +1,549 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: private_location/v1/private_location.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type MonitorsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MonitorsRequest) Reset() { + *x = MonitorsRequest{} + mi := &file_private_location_v1_private_location_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MonitorsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MonitorsRequest) ProtoMessage() {} + +func (x *MonitorsRequest) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_private_location_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MonitorsRequest.ProtoReflect.Descriptor instead. +func (*MonitorsRequest) Descriptor() ([]byte, []int) { + return file_private_location_v1_private_location_proto_rawDescGZIP(), []int{0} +} + +type MonitorsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + HttpMonitors []*HTTPMonitor `protobuf:"bytes,1,rep,name=http_monitors,json=httpMonitors,proto3" json:"http_monitors,omitempty"` + TcpMonitors []*TCPMonitor `protobuf:"bytes,2,rep,name=tcp_monitors,json=tcpMonitors,proto3" json:"tcp_monitors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MonitorsResponse) Reset() { + *x = MonitorsResponse{} + mi := &file_private_location_v1_private_location_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MonitorsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MonitorsResponse) ProtoMessage() {} + +func (x *MonitorsResponse) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_private_location_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MonitorsResponse.ProtoReflect.Descriptor instead. +func (*MonitorsResponse) Descriptor() ([]byte, []int) { + return file_private_location_v1_private_location_proto_rawDescGZIP(), []int{1} +} + +func (x *MonitorsResponse) GetHttpMonitors() []*HTTPMonitor { + if x != nil { + return x.HttpMonitors + } + return nil +} + +func (x *MonitorsResponse) GetTcpMonitors() []*TCPMonitor { + if x != nil { + return x.TcpMonitors + } + return nil +} + +type IngestTCPRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + MonitorId string `protobuf:"bytes,2,opt,name=monitorId,proto3" json:"monitorId,omitempty"` + Latency int64 `protobuf:"varint,3,opt,name=latency,proto3" json:"latency,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + CronTimestamp int64 `protobuf:"varint,5,opt,name=cronTimestamp,proto3" json:"cronTimestamp,omitempty"` + Uri string `protobuf:"bytes,6,opt,name=uri,proto3" json:"uri,omitempty"` + Message string `protobuf:"bytes,7,opt,name=message,proto3" json:"message,omitempty"` + RequestStatus string `protobuf:"bytes,8,opt,name=requestStatus,proto3" json:"requestStatus,omitempty"` + Error int64 `protobuf:"varint,9,opt,name=error,proto3" json:"error,omitempty"` + Timing string `protobuf:"bytes,10,opt,name=timing,proto3" json:"timing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IngestTCPRequest) Reset() { + *x = IngestTCPRequest{} + mi := &file_private_location_v1_private_location_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IngestTCPRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IngestTCPRequest) ProtoMessage() {} + +func (x *IngestTCPRequest) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_private_location_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IngestTCPRequest.ProtoReflect.Descriptor instead. +func (*IngestTCPRequest) Descriptor() ([]byte, []int) { + return file_private_location_v1_private_location_proto_rawDescGZIP(), []int{2} +} + +func (x *IngestTCPRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *IngestTCPRequest) GetMonitorId() string { + if x != nil { + return x.MonitorId + } + return "" +} + +func (x *IngestTCPRequest) GetLatency() int64 { + if x != nil { + return x.Latency + } + return 0 +} + +func (x *IngestTCPRequest) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *IngestTCPRequest) GetCronTimestamp() int64 { + if x != nil { + return x.CronTimestamp + } + return 0 +} + +func (x *IngestTCPRequest) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *IngestTCPRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *IngestTCPRequest) GetRequestStatus() string { + if x != nil { + return x.RequestStatus + } + return "" +} + +func (x *IngestTCPRequest) GetError() int64 { + if x != nil { + return x.Error + } + return 0 +} + +func (x *IngestTCPRequest) GetTiming() string { + if x != nil { + return x.Timing + } + return "" +} + +type IngestTCPResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IngestTCPResponse) Reset() { + *x = IngestTCPResponse{} + mi := &file_private_location_v1_private_location_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IngestTCPResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IngestTCPResponse) ProtoMessage() {} + +func (x *IngestTCPResponse) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_private_location_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IngestTCPResponse.ProtoReflect.Descriptor instead. +func (*IngestTCPResponse) Descriptor() ([]byte, []int) { + return file_private_location_v1_private_location_proto_rawDescGZIP(), []int{3} +} + +type IngestHTTPRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + MonitorId string `protobuf:"bytes,2,opt,name=monitorId,proto3" json:"monitorId,omitempty"` + Latency int64 `protobuf:"varint,3,opt,name=latency,proto3" json:"latency,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + CronTimestamp int64 `protobuf:"varint,5,opt,name=cronTimestamp,proto3" json:"cronTimestamp,omitempty"` + Url string `protobuf:"bytes,6,opt,name=url,proto3" json:"url,omitempty"` + RequestStatus string `protobuf:"bytes,7,opt,name=requestStatus,proto3" json:"requestStatus,omitempty"` + Message string `protobuf:"bytes,8,opt,name=message,proto3" json:"message,omitempty"` + Body string `protobuf:"bytes,9,opt,name=body,proto3" json:"body,omitempty"` + Headers string `protobuf:"bytes,10,opt,name=headers,proto3" json:"headers,omitempty"` + Timing string `protobuf:"bytes,11,opt,name=timing,proto3" json:"timing,omitempty"` + StatusCode int64 `protobuf:"varint,12,opt,name=statusCode,proto3" json:"statusCode,omitempty"` + Error int64 `protobuf:"varint,13,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IngestHTTPRequest) Reset() { + *x = IngestHTTPRequest{} + mi := &file_private_location_v1_private_location_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IngestHTTPRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IngestHTTPRequest) ProtoMessage() {} + +func (x *IngestHTTPRequest) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_private_location_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IngestHTTPRequest.ProtoReflect.Descriptor instead. +func (*IngestHTTPRequest) Descriptor() ([]byte, []int) { + return file_private_location_v1_private_location_proto_rawDescGZIP(), []int{4} +} + +func (x *IngestHTTPRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *IngestHTTPRequest) GetMonitorId() string { + if x != nil { + return x.MonitorId + } + return "" +} + +func (x *IngestHTTPRequest) GetLatency() int64 { + if x != nil { + return x.Latency + } + return 0 +} + +func (x *IngestHTTPRequest) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *IngestHTTPRequest) GetCronTimestamp() int64 { + if x != nil { + return x.CronTimestamp + } + return 0 +} + +func (x *IngestHTTPRequest) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *IngestHTTPRequest) GetRequestStatus() string { + if x != nil { + return x.RequestStatus + } + return "" +} + +func (x *IngestHTTPRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *IngestHTTPRequest) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *IngestHTTPRequest) GetHeaders() string { + if x != nil { + return x.Headers + } + return "" +} + +func (x *IngestHTTPRequest) GetTiming() string { + if x != nil { + return x.Timing + } + return "" +} + +func (x *IngestHTTPRequest) GetStatusCode() int64 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *IngestHTTPRequest) GetError() int64 { + if x != nil { + return x.Error + } + return 0 +} + +type IngestHTTPResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IngestHTTPResponse) Reset() { + *x = IngestHTTPResponse{} + mi := &file_private_location_v1_private_location_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IngestHTTPResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IngestHTTPResponse) ProtoMessage() {} + +func (x *IngestHTTPResponse) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_private_location_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IngestHTTPResponse.ProtoReflect.Descriptor instead. +func (*IngestHTTPResponse) Descriptor() ([]byte, []int) { + return file_private_location_v1_private_location_proto_rawDescGZIP(), []int{5} +} + +var File_private_location_v1_private_location_proto protoreflect.FileDescriptor + +const file_private_location_v1_private_location_proto_rawDesc = "" + + "\n" + + "*private_location/v1/private_location.proto\x12\x13private_location.v1\x1a&private_location/v1/http_monitor.proto\x1a%private_location/v1/tcp_monitor.proto\"\x11\n" + + "\x0fMonitorsRequest\"\x9d\x01\n" + + "\x10MonitorsResponse\x12E\n" + + "\rhttp_monitors\x18\x01 \x03(\v2 .private_location.v1.HTTPMonitorR\fhttpMonitors\x12B\n" + + "\ftcp_monitors\x18\x02 \x03(\v2\x1f.private_location.v1.TCPMonitorR\vtcpMonitors\"\x9e\x02\n" + + "\x10IngestTCPRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1c\n" + + "\tmonitorId\x18\x02 \x01(\tR\tmonitorId\x12\x18\n" + + "\alatency\x18\x03 \x01(\x03R\alatency\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\x12$\n" + + "\rcronTimestamp\x18\x05 \x01(\x03R\rcronTimestamp\x12\x10\n" + + "\x03uri\x18\x06 \x01(\tR\x03uri\x12\x18\n" + + "\amessage\x18\a \x01(\tR\amessage\x12$\n" + + "\rrequestStatus\x18\b \x01(\tR\rrequestStatus\x12\x14\n" + + "\x05error\x18\t \x01(\x03R\x05error\x12\x16\n" + + "\x06timing\x18\n" + + " \x01(\tR\x06timing\"\x13\n" + + "\x11IngestTCPResponse\"\xed\x02\n" + + "\x11IngestHTTPRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1c\n" + + "\tmonitorId\x18\x02 \x01(\tR\tmonitorId\x12\x18\n" + + "\alatency\x18\x03 \x01(\x03R\alatency\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\x12$\n" + + "\rcronTimestamp\x18\x05 \x01(\x03R\rcronTimestamp\x12\x10\n" + + "\x03url\x18\x06 \x01(\tR\x03url\x12$\n" + + "\rrequestStatus\x18\a \x01(\tR\rrequestStatus\x12\x18\n" + + "\amessage\x18\b \x01(\tR\amessage\x12\x12\n" + + "\x04body\x18\t \x01(\tR\x04body\x12\x18\n" + + "\aheaders\x18\n" + + " \x01(\tR\aheaders\x12\x16\n" + + "\x06timing\x18\v \x01(\tR\x06timing\x12\x1e\n" + + "\n" + + "statusCode\x18\f \x01(\x03R\n" + + "statusCode\x12\x14\n" + + "\x05error\x18\r \x01(\x03R\x05error\"\x14\n" + + "\x12IngestHTTPResponse2\xb2\x02\n" + + "\x16PrivateLocationService\x12Y\n" + + "\bMonitors\x12$.private_location.v1.MonitorsRequest\x1a%.private_location.v1.MonitorsResponse\"\x00\x12\\\n" + + "\tIngestTCP\x12%.private_location.v1.IngestTCPRequest\x1a&.private_location.v1.IngestTCPResponse\"\x00\x12_\n" + + "\n" + + "IngestHTTP\x12&.private_location.v1.IngestHTTPRequest\x1a'.private_location.v1.IngestHTTPResponse\"\x00BJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\x06proto3" + +var ( + file_private_location_v1_private_location_proto_rawDescOnce sync.Once + file_private_location_v1_private_location_proto_rawDescData []byte +) + +func file_private_location_v1_private_location_proto_rawDescGZIP() []byte { + file_private_location_v1_private_location_proto_rawDescOnce.Do(func() { + file_private_location_v1_private_location_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_private_location_proto_rawDesc), len(file_private_location_v1_private_location_proto_rawDesc))) + }) + return file_private_location_v1_private_location_proto_rawDescData +} + +var file_private_location_v1_private_location_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_private_location_v1_private_location_proto_goTypes = []any{ + (*MonitorsRequest)(nil), // 0: private_location.v1.MonitorsRequest + (*MonitorsResponse)(nil), // 1: private_location.v1.MonitorsResponse + (*IngestTCPRequest)(nil), // 2: private_location.v1.IngestTCPRequest + (*IngestTCPResponse)(nil), // 3: private_location.v1.IngestTCPResponse + (*IngestHTTPRequest)(nil), // 4: private_location.v1.IngestHTTPRequest + (*IngestHTTPResponse)(nil), // 5: private_location.v1.IngestHTTPResponse + (*HTTPMonitor)(nil), // 6: private_location.v1.HTTPMonitor + (*TCPMonitor)(nil), // 7: private_location.v1.TCPMonitor +} +var file_private_location_v1_private_location_proto_depIdxs = []int32{ + 6, // 0: private_location.v1.MonitorsResponse.http_monitors:type_name -> private_location.v1.HTTPMonitor + 7, // 1: private_location.v1.MonitorsResponse.tcp_monitors:type_name -> private_location.v1.TCPMonitor + 0, // 2: private_location.v1.PrivateLocationService.Monitors:input_type -> private_location.v1.MonitorsRequest + 2, // 3: private_location.v1.PrivateLocationService.IngestTCP:input_type -> private_location.v1.IngestTCPRequest + 4, // 4: private_location.v1.PrivateLocationService.IngestHTTP:input_type -> private_location.v1.IngestHTTPRequest + 1, // 5: private_location.v1.PrivateLocationService.Monitors:output_type -> private_location.v1.MonitorsResponse + 3, // 6: private_location.v1.PrivateLocationService.IngestTCP:output_type -> private_location.v1.IngestTCPResponse + 5, // 7: private_location.v1.PrivateLocationService.IngestHTTP:output_type -> private_location.v1.IngestHTTPResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_private_location_v1_private_location_proto_init() } +func file_private_location_v1_private_location_proto_init() { + if File_private_location_v1_private_location_proto != nil { + return + } + file_private_location_v1_http_monitor_proto_init() + file_private_location_v1_tcp_monitor_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_private_location_proto_rawDesc), len(file_private_location_v1_private_location_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_private_location_v1_private_location_proto_goTypes, + DependencyIndexes: file_private_location_v1_private_location_proto_depIdxs, + MessageInfos: file_private_location_v1_private_location_proto_msgTypes, + }.Build() + File_private_location_v1_private_location_proto = out.File + file_private_location_v1_private_location_proto_goTypes = nil + file_private_location_v1_private_location_proto_depIdxs = nil +} diff --git a/apps/checker/proto/private_location/v1/tcp_monitor.pb.go b/apps/checker/proto/private_location/v1/tcp_monitor.pb.go new file mode 100644 index 00000000..d81c9b7d --- /dev/null +++ b/apps/checker/proto/private_location/v1/tcp_monitor.pb.go @@ -0,0 +1,171 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: private_location/v1/tcp_monitor.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TCPMonitor struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Uri string `protobuf:"bytes,2,opt,name=uri,proto3" json:"uri,omitempty"` + Timeout int64 `protobuf:"varint,3,opt,name=timeout,proto3" json:"timeout,omitempty"` + DegradedAt *int64 `protobuf:"varint,4,opt,name=degraded_at,json=degradedAt,proto3,oneof" json:"degraded_at,omitempty"` + Periodicity string `protobuf:"bytes,5,opt,name=periodicity,proto3" json:"periodicity,omitempty"` + Retry int64 `protobuf:"varint,6,opt,name=retry,proto3" json:"retry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TCPMonitor) Reset() { + *x = TCPMonitor{} + mi := &file_private_location_v1_tcp_monitor_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TCPMonitor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TCPMonitor) ProtoMessage() {} + +func (x *TCPMonitor) ProtoReflect() protoreflect.Message { + mi := &file_private_location_v1_tcp_monitor_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TCPMonitor.ProtoReflect.Descriptor instead. +func (*TCPMonitor) Descriptor() ([]byte, []int) { + return file_private_location_v1_tcp_monitor_proto_rawDescGZIP(), []int{0} +} + +func (x *TCPMonitor) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TCPMonitor) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *TCPMonitor) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *TCPMonitor) GetDegradedAt() int64 { + if x != nil && x.DegradedAt != nil { + return *x.DegradedAt + } + return 0 +} + +func (x *TCPMonitor) GetPeriodicity() string { + if x != nil { + return x.Periodicity + } + return "" +} + +func (x *TCPMonitor) GetRetry() int64 { + if x != nil { + return x.Retry + } + return 0 +} + +var File_private_location_v1_tcp_monitor_proto protoreflect.FileDescriptor + +const file_private_location_v1_tcp_monitor_proto_rawDesc = "" + + "\n" + + "%private_location/v1/tcp_monitor.proto\x12\x13private_location.v1\"\xb6\x01\n" + + "\n" + + "TCPMonitor\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" + + "\x03uri\x18\x02 \x01(\tR\x03uri\x12\x18\n" + + "\atimeout\x18\x03 \x01(\x03R\atimeout\x12$\n" + + "\vdegraded_at\x18\x04 \x01(\x03H\x00R\n" + + "degradedAt\x88\x01\x01\x12 \n" + + "\vperiodicity\x18\x05 \x01(\tR\vperiodicity\x12\x14\n" + + "\x05retry\x18\x06 \x01(\x03R\x05retryB\x0e\n" + + "\f_degraded_atBJZHgithub.com/openstatushq/openstatus/packages/proto/private_location/v1;v1b\x06proto3" + +var ( + file_private_location_v1_tcp_monitor_proto_rawDescOnce sync.Once + file_private_location_v1_tcp_monitor_proto_rawDescData []byte +) + +func file_private_location_v1_tcp_monitor_proto_rawDescGZIP() []byte { + file_private_location_v1_tcp_monitor_proto_rawDescOnce.Do(func() { + file_private_location_v1_tcp_monitor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_private_location_v1_tcp_monitor_proto_rawDesc), len(file_private_location_v1_tcp_monitor_proto_rawDesc))) + }) + return file_private_location_v1_tcp_monitor_proto_rawDescData +} + +var file_private_location_v1_tcp_monitor_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_private_location_v1_tcp_monitor_proto_goTypes = []any{ + (*TCPMonitor)(nil), // 0: private_location.v1.TCPMonitor +} +var file_private_location_v1_tcp_monitor_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_private_location_v1_tcp_monitor_proto_init() } +func file_private_location_v1_tcp_monitor_proto_init() { + if File_private_location_v1_tcp_monitor_proto != nil { + return + } + file_private_location_v1_tcp_monitor_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_private_location_v1_tcp_monitor_proto_rawDesc), len(file_private_location_v1_tcp_monitor_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_private_location_v1_tcp_monitor_proto_goTypes, + DependencyIndexes: file_private_location_v1_tcp_monitor_proto_depIdxs, + MessageInfos: file_private_location_v1_tcp_monitor_proto_msgTypes, + }.Build() + File_private_location_v1_tcp_monitor_proto = out.File + file_private_location_v1_tcp_monitor_proto_goTypes = nil + file_private_location_v1_tcp_monitor_proto_depIdxs = nil +} diff --git a/apps/checker/request/request.go b/apps/checker/request/request.go index 6c1d5c4c..39bb409a 100644 --- a/apps/checker/request/request.go +++ b/apps/checker/request/request.go @@ -66,7 +66,7 @@ type HttpCheckerRequest struct { OtelConfig struct { Endpoint string `json:"endpoint"` Headers map[string]string `json:"headers,omitempty"` - } `json:"otelConfig,omitempty"` + } `json:"otelConfig"` } type TCPCheckerRequest struct { @@ -84,7 +84,7 @@ type TCPCheckerRequest struct { OtelConfig struct { Endpoint string `json:"endpoint"` Headers map[string]string `json:"headers,omitempty"` - } `json:"otelConfig,omitempty"` + } `json:"otelConfig"` } type TCPRequest struct { diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/edit/layout.tsx b/apps/dashboard/src/app/(dashboard)/monitors/[id]/edit/layout.tsx index 34360a88..45bdf55a 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/edit/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/edit/layout.tsx @@ -7,6 +7,7 @@ export default async function Layout({ }) { const queryClient = getQueryClient(); await queryClient.prefetchQuery(trpc.monitorTag.list.queryOptions()); + await queryClient.prefetchQuery(trpc.privateLocation.list.queryOptions()); return {children}; } diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/layout.tsx b/apps/dashboard/src/app/(dashboard)/monitors/[id]/layout.tsx index b881d52c..077c3af5 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/layout.tsx @@ -21,8 +21,8 @@ export default async function Layout({ await queryClient.prefetchQuery( trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }), ); - await queryClient.prefetchQuery(trpc.notification.list.queryOptions()); + await queryClient.prefetchQuery(trpc.privateLocation.list.queryOptions()); return ( diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/client.tsx b/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/client.tsx index 52e75286..1d7e4e48 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/client.tsx @@ -20,7 +20,7 @@ import { CommandRegion } from "@/components/controls-search/command-region"; import { DropdownStatus } from "@/components/controls-search/dropdown-status"; import { DropdownTrigger } from "@/components/controls-search/dropdown-trigger"; import { PopoverDate } from "@/components/controls-search/popover-date"; -import { columns } from "@/components/data-table/response-logs/columns"; +import { getColumns } from "@/components/data-table/response-logs/columns"; import { Sheet } from "@/components/data-table/response-logs/data-table-sheet"; import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; @@ -43,6 +43,9 @@ export function Client() { setSearchParams, ] = useQueryStates(searchParamsParsers); const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); + const { data: privateLocations } = useQuery( + trpc.privateLocation.list.queryOptions(), + ); const { data: monitor } = useQuery( trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }), ); @@ -75,6 +78,11 @@ export function Client() { [pageIndex, pageSize, setSearchParams], ); + const columns = useMemo( + () => getColumns(privateLocations ?? []), + [privateLocations], + ); + if (!workspace || !monitor) return null; return ( @@ -96,7 +104,12 @@ export function Client() { {monitor.jobType === "http" ? : null} - + + location.monitors.some((m) => m.id === Number(id)), + )} + /> @@ -132,6 +145,7 @@ export function Client() { )} setTimeout(() => setSearchParams({ selected: null }), 300) } @@ -142,6 +156,7 @@ export function Client() { } function BillingPlaceholder() { + const columns = useMemo(() => getColumns([]), []); return ( diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/search-params.ts b/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/search-params.ts index 8bc6264e..f9558f9b 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/search-params.ts +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/logs/search-params.ts @@ -1,5 +1,4 @@ import { PERIODS, STATUS, TRIGGER } from "@/data/metrics.client"; -import { monitorRegions } from "@openstatus/db/src/schema/constants"; import { endOfDay } from "date-fns"; import { startOfDay } from "date-fns"; import { @@ -13,10 +12,7 @@ import { export const searchParamsParsers = { period: parseAsStringLiteral(PERIODS).withDefault("1d"), - regions: parseAsArrayOf(parseAsStringLiteral(monitorRegions)).withDefault( - // FIXME: readonly - monitorRegions as unknown as (typeof monitorRegions)[number][], - ), + regions: parseAsArrayOf(parseAsString), status: parseAsStringLiteral(STATUS), trigger: parseAsStringLiteral(TRIGGER), selected: parseAsString, diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx b/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx index 0400eb54..b2b9914f 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx @@ -17,7 +17,7 @@ import { DropdownInterval } from "@/components/controls-search/dropdown-interval import { DropdownPercentile } from "@/components/controls-search/dropdown-percentile"; import { DropdownPeriod } from "@/components/controls-search/dropdown-period"; import { AuditLogsWrapper } from "@/components/data-table/audit-logs/wrapper"; -import { columns as regionColumns } from "@/components/data-table/response-logs/regions/columns"; +import { getColumns as getRegionColumns } from "@/components/data-table/response-logs/regions/columns"; import { GlobalUptimeSection } from "@/components/metric/global-uptime/section"; import { PopoverQuantile } from "@/components/popovers/popover-quantile"; import { PopoverResolution } from "@/components/popovers/popover-resolution"; @@ -30,7 +30,7 @@ import { monitorRegions } from "@openstatus/db/src/schema/constants"; import { useQuery } from "@tanstack/react-query"; import { useParams } from "next/navigation"; import { useQueryStates } from "nuqs"; -import React from "react"; +import React, { useMemo } from "react"; import { searchParamsParsers } from "./search-params"; const TIMELINE_INTERVAL = 30; // in days @@ -38,11 +38,16 @@ const TIMELINE_INTERVAL = 30; // in days export function Client() { const trpc = useTRPC(); const { id } = useParams<{ id: string }>(); - const [{ period, regions: selectedRegions, percentile, interval }] = + const [{ period, regions, percentile, interval }] = useQueryStates(searchParamsParsers); const { data: monitor } = useQuery( trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }), ); + const { data: privateLocations } = useQuery( + trpc.privateLocation.list.queryOptions(), + ); + + const selectedRegions = regions ?? undefined; const regionTimelineQuery = { ...trpc.tinybird.metricsRegions.queryOptions({ @@ -65,10 +70,19 @@ export function Client() { // once the data is loaded, we show all the regions that we get from TB isLoading ? monitor?.regions ?? [] - : (monitorRegions as unknown as (typeof monitorRegions)[number][]), + : [ + ...monitorRegions, + ...(privateLocations?.map((location) => location.id.toString()) ?? + []), + ], percentile, ); - }, [regionTimeline, monitor, percentile, isLoading]); + }, [regionTimeline, monitor, percentile, isLoading, privateLocations]); + + const regionColumns = useMemo( + () => getRegionColumns(privateLocations ?? []), + [privateLocations], + ); if (!monitor) return null; @@ -90,7 +104,10 @@ export function Client() {
including{" "} - +
@@ -185,7 +202,7 @@ export function Client() { columns={regionColumns} defaultPagination={{ pageIndex: 0, - pageSize: selectedRegions.length, + pageSize: selectedRegions?.length ?? 0, }} /> @@ -193,6 +210,7 @@ export function Client() { { region.trend.forEach((t) => { diff --git a/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/search-params.ts b/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/search-params.ts index 0c8c92f9..aa95cf64 100644 --- a/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/search-params.ts +++ b/apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/search-params.ts @@ -1,9 +1,9 @@ import { INTERVALS } from "@/data/metrics.client"; -import { monitorRegions } from "@openstatus/db/src/schema/constants"; import { createSearchParamsCache, parseAsArrayOf, parseAsNumberLiteral, + parseAsString, parseAsStringLiteral, } from "nuqs/server"; @@ -12,10 +12,7 @@ const PERCENTILE = ["p50", "p75", "p90", "p95", "p99"] as const; export const searchParamsParsers = { period: parseAsStringLiteral(PERIOD).withDefault("1d"), - regions: parseAsArrayOf(parseAsStringLiteral(monitorRegions)).withDefault( - // FIXME: readonly - monitorRegions as unknown as (typeof monitorRegions)[number][], - ), + regions: parseAsArrayOf(parseAsString), percentile: parseAsStringLiteral(PERCENTILE).withDefault("p50"), interval: parseAsNumberLiteral(INTERVALS).withDefault(30), }; diff --git a/apps/dashboard/src/app/(dashboard)/notifications/client.tsx b/apps/dashboard/src/app/(dashboard)/notifications/client.tsx index 63a36b55..d0e5908f 100644 --- a/apps/dashboard/src/app/(dashboard)/notifications/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/notifications/client.tsx @@ -57,6 +57,9 @@ export function Client() { Notifications + + Define your notifications to receive alerts when downtime occurs. +
{notifications.length === 0 ? ( diff --git a/apps/dashboard/src/app/(dashboard)/private-locations/breadcrumb.tsx b/apps/dashboard/src/app/(dashboard)/private-locations/breadcrumb.tsx new file mode 100644 index 00000000..1110c5a0 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/private-locations/breadcrumb.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { NavBreadcrumb } from "@/components/nav/nav-breadcrumb"; +import { Globe } from "lucide-react"; + +export function Breadcrumb() { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/private-locations/client.tsx b/apps/dashboard/src/app/(dashboard)/private-locations/client.tsx new file mode 100644 index 00000000..902d4c08 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/private-locations/client.tsx @@ -0,0 +1,104 @@ +"use client"; +import { Link } from "@/components/common/link"; +import { + BillingOverlay, + BillingOverlayButton, + BillingOverlayContainer, + BillingOverlayDescription, +} from "@/components/content/billing-overlay"; +import { + Section, + SectionDescription, + SectionGroup, + SectionHeader, + SectionTitle, +} from "@/components/content/section"; +import { columns } from "@/components/data-table/private-locations/columns"; +import { UpgradeDialog } from "@/components/dialogs/upgrade"; +import { DataTable } from "@/components/ui/data-table/data-table"; +import { useTRPC } from "@/lib/trpc/client"; +import type { RouterOutputs } from "@openstatus/api"; +import { useQuery } from "@tanstack/react-query"; +import { Lock } from "lucide-react"; +import { useState } from "react"; + +const EXAMPLES = [ + { + id: 1, + name: "Private Location 1", + token: "my-secret-token", + createdAt: new Date("2025-01-01"), + updatedAt: new Date("2025-01-01"), + workspaceId: 1, + lastSeenAt: new Date("2025-10-08"), + privateLocationToMonitors: [], + monitors: [], + }, + { + id: 2, + name: "Private Location 2", + token: "my-secret-token", + createdAt: new Date("2025-01-01"), + updatedAt: new Date("2025-01-01"), + workspaceId: 1, + lastSeenAt: new Date("2025-06-08"), + privateLocationToMonitors: [], + monitors: [], + }, +] satisfies RouterOutputs["privateLocation"]["list"]; + +export function Client() { + const trpc = useTRPC(); + const [openDialog, setOpenDialog] = useState(false); + const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); + const { data: privateLocations } = useQuery( + trpc.privateLocation.list.queryOptions(), + ); + + if (!privateLocations || !workspace) return null; + + return ( + + + Private Locations + + Create and manage your private locations. + + +
+ {workspace.limits["private-locations"] === false ? ( + + + + setOpenDialog(true)}> + + Upgrade + + + Create private locations to monitor your internal services.{" "} + + Learn more + + . + + + + + ) : ( + + )} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/private-locations/layout.tsx b/apps/dashboard/src/app/(dashboard)/private-locations/layout.tsx new file mode 100644 index 00000000..07110b53 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/private-locations/layout.tsx @@ -0,0 +1,33 @@ +import { + AppHeader, + AppHeaderActions, + AppHeaderContent, +} from "@/components/nav/app-header"; +import { AppSidebarTrigger } from "@/components/nav/app-sidebar"; +import { HydrateClient, getQueryClient, trpc } from "@/lib/trpc/server"; +import { Breadcrumb } from "./breadcrumb"; +import { NavActions } from "./nav-actions"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(trpc.notification.list.queryOptions()); + await queryClient.prefetchQuery(trpc.privateLocation.list.queryOptions()); + return ( + + + + + + + + + + +
{children}
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/private-locations/nav-actions.tsx b/apps/dashboard/src/app/(dashboard)/private-locations/nav-actions.tsx new file mode 100644 index 00000000..f97dcd7e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/private-locations/nav-actions.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { UpgradeDialog } from "@/components/dialogs/upgrade"; +import { FormSheetPrivateLocation } from "@/components/forms/private-location/sheet"; +import { NavFeedback } from "@/components/nav/nav-feedback"; +import { Button } from "@/components/ui/button"; +import { useTRPC } from "@/lib/trpc/client"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +export function NavActions() { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [openDialog, setOpenDialog] = useState(false); + const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); + const { data: monitors } = useQuery(trpc.monitor.list.queryOptions()); + const createPrivateLocationMutation = useMutation( + trpc.privateLocation.new.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.privateLocation.list.queryKey(), + }); + }, + }), + ); + + if (!workspace || !monitors) return null; + + const limitReached = !workspace.limits["private-locations"]; + + return ( +
+ + {limitReached ? ( + + ) : ( + { + await createPrivateLocationMutation.mutateAsync({ + name: values.name, + monitors: values.monitors, + }); + }} + > + + + )} + +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/private-locations/page.tsx b/apps/dashboard/src/app/(dashboard)/private-locations/page.tsx new file mode 100644 index 00000000..961923e4 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/private-locations/page.tsx @@ -0,0 +1,10 @@ +import type { SearchParams } from "nuqs"; +import { Client } from "./client"; + +export default async function Page({ + searchParams, +}: { + searchParams: Promise; +}) { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx index af9c2e1c..e3e5633f 100644 --- a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx @@ -21,11 +21,13 @@ import { import { Section } from "@/components/content/section"; import { columns } from "@/components/data-table/subscribers/columns"; +import { UpgradeDialog } from "@/components/dialogs/upgrade"; import { DataTable } from "@/components/ui/data-table/data-table"; import { useTRPC } from "@/lib/trpc/client"; import { useQuery } from "@tanstack/react-query"; import { Lock } from "lucide-react"; import { useParams } from "next/navigation"; +import { useState } from "react"; const EXAMPLES = [ { @@ -52,6 +54,7 @@ const EXAMPLES = [ export default function Page() { const { id } = useParams<{ id: string }>(); + const [openDialog, setOpenDialog] = useState(false); const trpc = useTRPC(); const { data: page } = useQuery( trpc.page.get.queryOptions({ id: Number.parseInt(id) }), @@ -79,7 +82,7 @@ export default function Page() { data={[...EXAMPLES, ...EXAMPLES, ...EXAMPLES]} /> - + setOpenDialog(true)}> Upgrade @@ -95,6 +98,11 @@ export default function Page() { . + ) : subscribers?.length ? ( diff --git a/apps/dashboard/src/components/chart/chart-area-latency.tsx b/apps/dashboard/src/components/chart/chart-area-latency.tsx index 55c92cb0..710953f9 100644 --- a/apps/dashboard/src/components/chart/chart-area-latency.tsx +++ b/apps/dashboard/src/components/chart/chart-area-latency.tsx @@ -20,7 +20,6 @@ import { } from "@/components/ui/chart"; import { type PERCENTILES, mapLatency } from "@/data/metrics.client"; import { useTRPC } from "@/lib/trpc/client"; -import type { monitorRegions } from "@openstatus/db/src/schema/constants"; import { useQuery } from "@tanstack/react-query"; import { ChartTooltipNumber } from "./chart-tooltip-number"; @@ -46,7 +45,7 @@ export function ChartAreaLatency({ percentile: (typeof PERCENTILES)[number]; period: "1d" | "7d" | "14d"; type: "http" | "tcp"; - regions: (typeof monitorRegions)[number][]; + regions: string[] | undefined; }) { const trpc = useTRPC(); diff --git a/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx b/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx index f6488d90..4368915d 100644 --- a/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx +++ b/apps/dashboard/src/components/chart/chart-area-timing-phases.tsx @@ -24,7 +24,6 @@ import { mapTimingPhases, } from "@/data/metrics.client"; import { useTRPC } from "@/lib/trpc/client"; -import type { monitorRegions } from "@openstatus/db/src/schema/constants"; import { useQuery } from "@tanstack/react-query"; import { ChartTooltipNumber, @@ -70,7 +69,7 @@ export function ChartAreaTimingPhases({ period: "1d" | "7d" | "14d"; percentile: (typeof PERCENTILES)[number]; interval: (typeof INTERVALS)[number]; - regions: (typeof monitorRegions)[number][]; + regions: string[] | undefined; type: "http"; }) { const trpc = useTRPC(); diff --git a/apps/dashboard/src/components/chart/chart-bar-uptime.tsx b/apps/dashboard/src/components/chart/chart-bar-uptime.tsx index ca7431b1..51eb6793 100644 --- a/apps/dashboard/src/components/chart/chart-bar-uptime.tsx +++ b/apps/dashboard/src/components/chart/chart-bar-uptime.tsx @@ -13,7 +13,6 @@ import { import { type PERIODS, mapUptime } from "@/data/metrics.client"; import { useIsMobile } from "@/hooks/use-mobile"; import { useTRPC } from "@/lib/trpc/client"; -import type { Region } from "@openstatus/db/src/schema/constants"; import { useQuery } from "@tanstack/react-query"; import { endOfDay, startOfDay, subDays } from "date-fns"; @@ -53,7 +52,7 @@ export function ChartBarUptime({ monitorId: string; period: (typeof PERIODS)[number]; type: "http" | "tcp"; - regions: Region[]; + regions: string[] | undefined; }) { const isMobile = useIsMobile(); const trpc = useTRPC(); diff --git a/apps/dashboard/src/components/chart/chart-line-regions.tsx b/apps/dashboard/src/components/chart/chart-line-regions.tsx index f4e00bf2..ff9c9d3f 100644 --- a/apps/dashboard/src/components/chart/chart-line-regions.tsx +++ b/apps/dashboard/src/components/chart/chart-line-regions.tsx @@ -10,26 +10,37 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; -import { regionColors } from "@/data/regions"; +import { getRegionColor } from "@/data/regions"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; +import type { PrivateLocation } from "@openstatus/db/src/schema"; import { monitorRegions } from "@openstatus/db/src/schema/constants"; -import { regionDict } from "@openstatus/regions"; +import { getRegionInfo } from "@openstatus/regions"; import { ChartTooltipNumber } from "./chart-tooltip-number"; -const chartConfig = monitorRegions.reduce((config, region) => { - const regionInfo = regionDict[region as keyof typeof regionDict]; - const color = regionColors[region as keyof typeof regionColors]; +function getChartConfig(privateLocations?: PrivateLocation[]) { + return [ + ...monitorRegions, + ...(privateLocations?.map((location) => location.id.toString()) ?? []), + ].reduce((config, region) => { + const privateLocation = privateLocations?.find( + (location) => String(location.id) === String(region), + ); + const regionInfo = getRegionInfo(region, { + location: privateLocation?.name, + }); + const color = getRegionColor(region); - if (regionInfo && color) { - config[region] = { - label: `${regionInfo.location} (${regionInfo.provider})`, - color, - }; - } + if (regionInfo && color) { + config[region] = { + label: `${regionInfo.location} (${regionInfo.provider})`, + color, + }; + } - return config; -}, {} as ChartConfig) satisfies ChartConfig; + return config; + }, {} as ChartConfig) satisfies ChartConfig; +} export type TrendPoint = { timestamp: number; // unix millis @@ -40,14 +51,16 @@ export function ChartLineRegions({ className, data, regions, + privateLocations, }: { className?: string; data: TrendPoint[]; - regions: string[]; + regions: string[] | undefined; + privateLocations?: PrivateLocation[]; }) { const isMobile = useIsMobile(); const trendData = data ?? []; - + const chartConfig = getChartConfig(privateLocations); const chartData = trendData.map((d) => ({ ...d, timestamp: new Date(d.timestamp).toLocaleString("default", { @@ -87,7 +100,7 @@ export function ChartLineRegions({ /> } /> - {regions.map((region) => { + {regions?.map((region) => { return ( `${value}ms`} /> - {regions.length <= 6 && !isMobile ? ( + {regions && regions.length <= 6 && !isMobile ? ( } diff --git a/apps/dashboard/src/components/controls-search/command-region.tsx b/apps/dashboard/src/components/controls-search/command-region.tsx index 7fc910c9..a9e49887 100644 --- a/apps/dashboard/src/components/controls-search/command-region.tsx +++ b/apps/dashboard/src/components/controls-search/command-region.tsx @@ -22,29 +22,29 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { REGIONS } from "@/data/metrics.client"; +import type { REGIONS } from "@/data/metrics.client"; import { useTRPC } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { formatRegionCode, groupByContinent } from "@openstatus/regions"; import { useQuery } from "@tanstack/react-query"; -import { Check, Lock } from "lucide-react"; -import { parseAsArrayOf, parseAsStringLiteral, useQueryState } from "nuqs"; - -export const parseRegions = (regions: (typeof REGIONS)[number][]) => - parseAsArrayOf( - parseAsStringLiteral(REGIONS.filter((region) => regions.includes(region))), - ).withDefault(regions as unknown as (typeof REGIONS)[number][]); +import { Check, Globe, Lock } from "lucide-react"; +import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"; export function CommandRegion({ regions, + privateLocations, }: { regions: (typeof REGIONS)[number][]; + privateLocations?: { id: number; name: string }[]; }) { const trpc = useTRPC(); const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); const [selectedRegions, setSelectedRegions] = useQueryState( "regions", - parseRegions(regions), + parseAsArrayOf(parseAsString).withDefault([ + ...regions, + ...(privateLocations?.map((location) => location.id.toString()) ?? []), + ]), ); const limited = workspace?.plan === "free"; @@ -148,6 +148,35 @@ export function CommandRegion({ ); }, )} + {privateLocations && privateLocations.length > 0 ? ( + + {privateLocations.map((location) => ( + { + setSelectedRegions((prev) => + prev.includes(location.id.toString()) + ? prev.filter((r) => r !== location.id.toString()) + : [...prev, location.id.toString()], + ); + }} + > + + {location.name} + + + ))} + + ) : null} No region found. diff --git a/apps/dashboard/src/components/data-table/audit-logs/columns.tsx b/apps/dashboard/src/components/data-table/audit-logs/columns.tsx index 39554816..83798aed 100644 --- a/apps/dashboard/src/components/data-table/audit-logs/columns.tsx +++ b/apps/dashboard/src/components/data-table/audit-logs/columns.tsx @@ -2,94 +2,104 @@ import { HoverCardTimestamp } from "@/components/common/hover-card-timestamp"; import { TableCellDate } from "@/components/data-table/table-cell-date"; -import { config, metadata } from "@/data/audit-logs.client"; +import { config, getMetadata } from "@/data/audit-logs.client"; import { cn } from "@/lib/utils"; import type { RouterOutputs } from "@openstatus/api"; +import type { PrivateLocation } from "@openstatus/db/src/schema"; import type { ColumnDef } from "@tanstack/react-table"; type AuditLog = RouterOutputs["tinybird"]["auditLog"]["data"][number]; -export const columns: ColumnDef[] = [ - { - id: "icon", - accessorFn: (row) => row.action, - header: () => null, - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const value = row.getValue("action"); - const { icon: Icon, color } = config[value as keyof typeof config]; - if (!Icon) return null; - return ; +export function getColumns( + privateLocations?: PrivateLocation[], +): ColumnDef[] { + const metadata = getMetadata(privateLocations); + return [ + { + id: "icon", + accessorFn: (row) => row.action, + header: () => null, + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const value = row.getValue("action"); + const { icon: Icon, color } = config[value as keyof typeof config]; + if (!Icon) return null; + return ; + }, + meta: { + headerClassName: "w-7", + }, }, - meta: { - headerClassName: "w-7", + { + accessorKey: "action", + header: "Action", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const value = row.getValue("action"); + const { title } = config[value as keyof typeof config]; + if (!title) return null; + return
{title}
; + }, }, - }, - { - accessorKey: "action", - header: "Action", - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const value = row.getValue("action"); - const { title } = config[value as keyof typeof config]; - if (!title) return null; - return
{title}
; - }, - }, - { - accessorKey: "metadata", - header: "Information", - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const value = row.getValue("metadata"); - if (!value) return null; - return ( -
- {Object.entries(value) - .filter(([key, value]) => - metadata[key as keyof typeof metadata]?.visible(value), - ) - .map(([key, value]) => ( - - ))} -
- ); + { + accessorKey: "metadata", + header: "Information", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const value = row.getValue("metadata"); + if (!value) return null; + return ( +
+ {Object.entries(value) + .filter(([key, value]) => + metadata[key as keyof typeof metadata]?.visible(value), + ) + .map(([key, value]) => { + return ( + + ); + })} +
+ ); + }, }, - }, - { - accessorKey: "timestamp", - header: "Timestamp", - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const value = row.getValue("timestamp"); - if (value instanceof Date) { + { + accessorKey: "timestamp", + header: "Timestamp", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const value = row.getValue("timestamp"); + if (value instanceof Date) { + return ( + + + + ); + } + const date = new Date(Number(value)); + if (Number.isNaN(date.getTime())) { + return
{String(value)}
; + } + return ( - - + + ); - } - const date = new Date(Number(value)); - if (Number.isNaN(date.getTime())) { - return
{String(value)}
; - } - - return ( - - - - ); + }, }, - }, -]; + ]; +} function Pill({ label, value }: { label: string; value?: string }) { if (!value) return null; diff --git a/apps/dashboard/src/components/data-table/audit-logs/wrapper.tsx b/apps/dashboard/src/components/data-table/audit-logs/wrapper.tsx index 4395dfc7..001d1b5a 100644 --- a/apps/dashboard/src/components/data-table/audit-logs/wrapper.tsx +++ b/apps/dashboard/src/components/data-table/audit-logs/wrapper.tsx @@ -12,7 +12,8 @@ import { Skeleton } from "@/components/ui/skeleton"; import { useTRPC } from "@/lib/trpc/client"; import { useQuery } from "@tanstack/react-query"; -import { columns } from "./columns"; +import { useMemo } from "react"; +import { getColumns } from "./columns"; export function AuditLogsWrapper({ monitorId, @@ -27,6 +28,15 @@ export function AuditLogsWrapper({ trpc.tinybird.auditLog.queryOptions({ monitorId, interval }), ); + const { data: privateLocations } = useQuery( + trpc.privateLocation.list.queryOptions(), + ); + + const columns = useMemo( + () => getColumns(privateLocations), + [privateLocations], + ); + if (isLoading) { return (
diff --git a/apps/dashboard/src/components/data-table/private-locations/columns.tsx b/apps/dashboard/src/components/data-table/private-locations/columns.tsx new file mode 100644 index 00000000..d5611303 --- /dev/null +++ b/apps/dashboard/src/components/data-table/private-locations/columns.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { DataTableColumnHeader } from "@/components/ui/data-table/data-table-column-header"; +import type { RouterOutputs } from "@openstatus/api"; +import type { ColumnDef } from "@tanstack/react-table"; +import { TableCellBadge } from "../table-cell-badge"; +import { TableCellDate } from "../table-cell-date"; +import { DataTableRowActions } from "./data-table-row-actions"; + +type PrivateLocation = RouterOutputs["privateLocation"]["list"][number]; + +export const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + enableHiding: false, + }, + { + accessorKey: "lastSeenAt", + header: ({ column }) => ( + + ), + enableHiding: false, + cell: ({ row }) => { + const value = row.getValue("lastSeenAt"); + return ; + }, + }, + { + accessorKey: "monitors", + header: "Monitors", + enableSorting: false, + enableHiding: false, + + cell: ({ row }) => { + const value = row.getValue("monitors"); + if (Array.isArray(value) && value.length > 0 && "name" in value[0]) { + return ( +
+ {value.map((m) => ( + + ))} +
+ ); + } + return -; + }, + meta: { + cellClassName: "tabular-nums font-mono", + }, + }, + { + id: "actions", + cell: ({ row }) => , + meta: { + cellClassName: "w-8", + }, + }, +]; diff --git a/apps/dashboard/src/components/data-table/private-locations/data-table-row-actions.tsx b/apps/dashboard/src/components/data-table/private-locations/data-table-row-actions.tsx new file mode 100644 index 00000000..e5282afd --- /dev/null +++ b/apps/dashboard/src/components/data-table/private-locations/data-table-row-actions.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { QuickActions } from "@/components/dropdowns/quick-actions"; +import { FormSheetPrivateLocation } from "@/components/forms/private-location/sheet"; +import { getActions } from "@/data/notifications.client"; +import { useTRPC } from "@/lib/trpc/client"; +import type { RouterOutputs } from "@openstatus/api"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { Row } from "@tanstack/react-table"; +import { useRef } from "react"; + +type PrivateLocation = RouterOutputs["privateLocation"]["list"][number]; + +interface DataTableRowActionsProps { + row: Row; +} + +export function DataTableRowActions(props: DataTableRowActionsProps) { + const buttonRef = useRef(null); + const actions = getActions({ + edit: () => buttonRef.current?.click(), + }); + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const { data: monitors } = useQuery(trpc.monitor.list.queryOptions()); + const updatePrivateLocationMutation = useMutation( + trpc.privateLocation.update.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.privateLocation.list.queryKey(), + }); + }, + }), + ); + const deletePrivateLocationMutation = useMutation( + trpc.privateLocation.delete.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.privateLocation.list.queryKey(), + }); + }, + }), + ); + + return ( + <> + { + await deletePrivateLocationMutation.mutateAsync({ + id: props.row.original.id, + }); + }, + }} + /> + m.id), + }} + monitors={monitors ?? []} + onSubmit={async (values) => { + await updatePrivateLocationMutation.mutateAsync({ + id: props.row.original.id, + name: values.name, + monitors: values.monitors, + }); + }} + > + + + + ); +} diff --git a/apps/dashboard/src/components/data-table/response-logs/columns.tsx b/apps/dashboard/src/components/data-table/response-logs/columns.tsx index 9b8e5c80..e504ee36 100644 --- a/apps/dashboard/src/components/data-table/response-logs/columns.tsx +++ b/apps/dashboard/src/components/data-table/response-logs/columns.tsx @@ -17,144 +17,158 @@ import { import { getStatusCodeVariant, textColors } from "@/data/status-codes"; import { cn } from "@/lib/utils"; import type { RouterOutputs } from "@openstatus/api"; -import { regionDict } from "@openstatus/regions"; +import type { PrivateLocation } from "@openstatus/db/src/schema"; +import { getRegionInfo } from "@openstatus/regions"; import { HoverCardPortal } from "@radix-ui/react-hover-card"; import type { ColumnDef } from "@tanstack/react-table"; import { Clock, Workflow } from "lucide-react"; type ResponseLog = RouterOutputs["tinybird"]["list"]["data"][number]; -export const columns: ColumnDef[] = [ - { - accessorKey: "requestStatus", - header: () => null, - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const value = row.getValue("requestStatus"); - if (value === "error") { - return
; - } - if (value === "degraded") { - return
; - } - if (value === "success") { - return
; - } - return
-
; - }, - }, - { - accessorKey: "timestamp", - header: "Timestamp", - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const value = new Date(row.getValue("timestamp")); - return ( - - - - ); +// export const columns: ColumnDef[] = +export function getColumns( + privateLocations: PrivateLocation[], +): ColumnDef[] { + return [ + { + accessorKey: "requestStatus", + header: () => null, + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const value = row.getValue("requestStatus"); + if (value === "error") { + return
; + } + if (value === "degraded") { + return
; + } + if (value === "success") { + return
; + } + return
-
; + }, }, - }, - { - accessorKey: "statusCode", - header: "Status", - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - const log = row.original; - if (log.type === "http") { - const value = log.statusCode; - const variant = getStatusCodeVariant(value); + { + accessorKey: "timestamp", + header: "Timestamp", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const value = new Date(row.getValue("timestamp")); return ( - + + + ); - } - return
-
; - }, - }, - { - accessorKey: "latency", - header: "Latency", - enableSorting: false, - enableHiding: false, - cell: ({ row }) => { - return ; + }, }, - }, - { - accessorKey: "region", - header: "Region", - cell: ({ row }) => { - const value = row.getValue("region"); - - if (typeof value !== "string") { + { + accessorKey: "statusCode", + header: "Status", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const log = row.original; + if (log.type === "http") { + const value = log.statusCode; + const variant = getStatusCodeVariant(value); + return ( + + ); + } return
-
; - } - - const regionConfig = regionDict[value as keyof typeof regionDict]; - return ( -
- {regionConfig.location}{" "} - - ({regionConfig.provider}) - -
- ); - }, - enableSorting: false, - enableHiding: false, - filterFn: "arrIncludesSome", - meta: { - cellClassName: "text-muted-foreground font-mono", + }, }, - }, - { - accessorKey: "timing", - header: "Timing", - cell: ({ row }) => { - const log = row.original; - if (log.type === "http" && log.timing) { - return ; - } - return
-
; + { + accessorKey: "latency", + header: "Latency", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + return ; + }, }, - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "trigger", - header: "Trigger", - cell: ({ row }) => { - const value = row.getValue("trigger"); - if (value === "cron" || value === "api") { - const Icon = value === "cron" ? Clock : Workflow; - const label = value === "cron" ? "Scheduled" : "API"; + { + accessorKey: "region", + header: "Region", + cell: ({ row }) => { + const value = row.getValue("region"); + + if (typeof value !== "string") { + return
-
; + } + + const regionConfig = getRegionInfo(value, { + location: privateLocations.find( + (location) => String(location.id) === String(value), + )?.name, + }); + return ( - - - - - - -

{label}

-
-
-
+
+ {regionConfig.location}{" "} + + ({regionConfig.provider}) + +
); - } - return
-
; + }, + enableSorting: false, + enableHiding: false, + filterFn: "arrIncludesSome", + meta: { + cellClassName: "text-muted-foreground font-mono", + }, }, - enableSorting: false, - enableHiding: false, - meta: { - cellClassName: "font-mono", - headerClassName: "sr-only", + { + accessorKey: "timing", + header: "Timing", + cell: ({ row }) => { + const log = row.original; + if (log.type === "http" && log.timing) { + return ; + } + return
-
; + }, + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "trigger", + header: "Trigger", + cell: ({ row }) => { + const value = row.getValue("trigger"); + if (value === "cron" || value === "api") { + const Icon = value === "cron" ? Clock : Workflow; + const label = value === "cron" ? "Scheduled" : "API"; + return ( + + + + + + +

{label}

+
+
+
+ ); + } + return
-
; + }, + enableSorting: false, + enableHiding: false, + meta: { + cellClassName: "font-mono", + headerClassName: "sr-only", + }, }, - }, -]; + ]; +} function HoverCardTiming({ timing, diff --git a/apps/dashboard/src/components/data-table/response-logs/data-table-basics.tsx b/apps/dashboard/src/components/data-table/response-logs/data-table-basics.tsx index b6b2382e..d08ddf09 100644 --- a/apps/dashboard/src/components/data-table/response-logs/data-table-basics.tsx +++ b/apps/dashboard/src/components/data-table/response-logs/data-table-basics.tsx @@ -16,29 +16,47 @@ import { getStatusCodeVariant, textColors } from "@/data/status-codes"; import { formatMilliseconds, formatPercentage } from "@/lib/formatter"; import { cn } from "@/lib/utils"; import type { RouterOutputs } from "@openstatus/api"; -import { regionDict } from "@openstatus/regions"; +import type { PrivateLocation } from "@openstatus/db/src/schema"; +import { getRegionInfo } from "@openstatus/regions"; import { Braces, TableProperties } from "lucide-react"; type ResponseLog = RouterOutputs["tinybird"]["get"]["data"][number]; -export function DataTableBasics({ data }: { data: ResponseLog }) { +export function DataTableBasics({ + data, + privateLocations, +}: { + data: ResponseLog; + privateLocations?: PrivateLocation[]; +}) { if (data.type === "http") { - return ; + return ( + + ); } if (data.type === "tcp") { - return ; + return ( + + ); } return null; } export function DataTableBasicsHTTP({ data, + privateLocations, }: { data: Extract & { trigger?: "cron" | "api" | "test" | null; }; + privateLocations?: PrivateLocation[]; }) { - const regionConfig = regionDict[data.region]; + const privateLocataion = privateLocations?.find( + (location) => String(location.id) === String(data.region), + ); + const regionConfig = getRegionInfo(data.region, { + location: privateLocataion?.name, + }); return ( @@ -304,12 +322,19 @@ export function DataTableBasicsHTTP({ export function DataTableBasicsTCP({ data, + privateLocations, }: { data: Extract & { trigger?: "cron" | "api" | "test" | null; }; + privateLocations?: PrivateLocation[]; }) { - const regionConfig = regionDict[data.region]; + const privateLocataion = privateLocations?.find( + (location) => String(location.id) === String(data.region), + ); + const regionConfig = getRegionInfo(data.region, { + location: privateLocataion?.name, + }); return (
diff --git a/apps/dashboard/src/components/data-table/response-logs/data-table-sheet.tsx b/apps/dashboard/src/components/data-table/response-logs/data-table-sheet.tsx index 0b51f405..9a39677e 100644 --- a/apps/dashboard/src/components/data-table/response-logs/data-table-sheet.tsx +++ b/apps/dashboard/src/components/data-table/response-logs/data-table-sheet.tsx @@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; import type { RouterOutputs } from "@openstatus/api"; +import type { PrivateLocation } from "@openstatus/db/src/schema"; import { Check, Copy } from "lucide-react"; import { DataTableBasics } from "./data-table-basics"; @@ -18,9 +19,11 @@ type ResponseLog = RouterOutputs["tinybird"]["get"]["data"][number]; export function Sheet({ data, + privateLocations, onClose, }: { data: ResponseLog | null; + privateLocations?: PrivateLocation[]; onClose: () => void; }) { const { copy, isCopied } = useCopyToClipboard(); @@ -32,7 +35,7 @@ export function Sheet({ Response Logs - + + +
+ {privateLocations.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange([ + ...field.value, + item.id, + ]) + : field.onChange( + field.value?.filter( + (value) => value !== item.id, + ), + ); + }} + /> + + + {item.name} + + + + ); + }} + /> + ))} +
+ + + )} + /> + )} + Your plan allows you to run{" "} diff --git a/apps/dashboard/src/components/forms/monitor/update.tsx b/apps/dashboard/src/components/forms/monitor/update.tsx index 84557486..5789afce 100644 --- a/apps/dashboard/src/components/forms/monitor/update.tsx +++ b/apps/dashboard/src/components/forms/monitor/update.tsx @@ -26,6 +26,9 @@ export function FormMonitorUpdate() { trpc.monitor.get.queryOptions({ id: Number.parseInt(id) }), ); const { data: statusPages } = useQuery(trpc.page.list.queryOptions()); + const { data: privateLocations } = useQuery( + trpc.privateLocation.list.queryOptions(), + ); const { data: notifications } = useQuery( trpc.notification.list.queryOptions(), ); @@ -105,7 +108,14 @@ export function FormMonitorUpdate() { }), ); - if (!monitor || !statusPages || !notifications || !workspace) return null; + if ( + !monitor || + !statusPages || + !notifications || + !workspace || + !privateLocations + ) + return null; return ( @@ -166,15 +176,18 @@ export function FormMonitorUpdate() { }} /> id), }} onSubmit={async (values) => { await updateSchedulingRegionsMutation.mutateAsync({ id: Number.parseInt(id), regions: values.regions, periodicity: values.periodicity, + privateLocations: values.privateLocations, }); }} /> diff --git a/apps/dashboard/src/components/forms/private-location/form.tsx b/apps/dashboard/src/components/forms/private-location/form.tsx new file mode 100644 index 00000000..91837f40 --- /dev/null +++ b/apps/dashboard/src/components/forms/private-location/form.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { + EmptyStateContainer, + EmptyStateTitle, +} from "@/components/content/empty-state"; +import { + FormCardContent, + FormCardSeparator, +} from "@/components/forms/form-card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group"; +import { Label } from "@/components/ui/label"; +import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { isTRPCClientError } from "@trpc/client"; +import { Check, Copy } from "lucide-react"; +import type React from "react"; +import { useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const schema = z.object({ + name: z.string().min(1, "Name is required"), + key: z.string(), + monitors: z.array(z.number()), +}); + +export type FormValues = z.infer; + +export function FormPrivateLocation({ + defaultValues, + onSubmit, + className, + monitors, + ...props +}: Omit, "onSubmit"> & { + defaultValues?: FormValues; + monitors: { id: number; name: string; url: string }[]; + onSubmit: (values: FormValues) => Promise; +}) { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaultValues ?? { + name: "", + key: crypto.randomUUID(), + monitors: [], + }, + }); + const [isPending, startTransition] = useTransition(); + const { copy, isCopied } = useCopyToClipboard(); + + function submitAction(values: FormValues) { + if (isPending) return; + + startTransition(async () => { + try { + const promise = onSubmit(values); + toast.promise(promise, { + loading: "Saving...", + success: () => "Saved", + error: (error) => { + if (isTRPCClientError(error)) { + return error.message; + } + return "Failed to save"; + }, + }); + await promise; + } catch (error) { + console.error(error); + } + }); + } + + return ( +
+ + + ( + + Name + + + + + + )} + /> + + + + ( + + Key + + + + + { + copy(field.value, { + successMessage: "Key copied to clipboard", + }); + }} + > + {isCopied ? : } + + + + + + + )} + /> + + + + ( + + Monitors + + Connected monitors will be automatically activated for the + private location. + + {monitors.length ? ( +
+
+ + { + field.onChange( + checked ? monitors.map((m) => m.id) : [], + ); + }} + /> + + +
+ {monitors.map((item) => ( +
+ + { + const newValue = checked + ? [...(field.value || []), item.id] + : field.value?.filter((id) => id !== item.id); + field.onChange(newValue); + }} + /> + + +
+ ))} +
+ ) : ( + + No monitors found + + )} + +
+ )} + /> +
+ + + ); +} diff --git a/apps/dashboard/src/components/forms/private-location/sheet.tsx b/apps/dashboard/src/components/forms/private-location/sheet.tsx new file mode 100644 index 00000000..c1f3f7cb --- /dev/null +++ b/apps/dashboard/src/components/forms/private-location/sheet.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { FormCard, FormCardGroup } from "@/components/forms/form-card"; +import { + FormSheet, + FormSheetContent, + FormSheetDescription, + FormSheetFooter, + FormSheetHeader, + FormSheetTitle, + FormSheetTrigger, +} from "@/components/forms/form-sheet"; +import { + FormPrivateLocation, + type FormValues, +} from "@/components/forms/private-location/form"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; + +export function FormSheetPrivateLocation({ + children, + defaultValues, + onSubmit, + monitors, + ...props +}: Omit, "onSubmit"> & { + defaultValues?: FormValues; + monitors: { id: number; name: string; url: string }[]; + onSubmit: (values: FormValues) => Promise; +}) { + const [open, setOpen] = useState(false); + + return ( + + + {children} + + + + Private Location + + Configure and update the private location. + + + + + { + await onSubmit(values); + setOpen(false); + }} + defaultValues={defaultValues} + id="private-location-form" + className="my-4" + /> + + + + + + + + ); +} diff --git a/apps/dashboard/src/components/metric/global-uptime/section.tsx b/apps/dashboard/src/components/metric/global-uptime/section.tsx index 0ac2f997..c7d86363 100644 --- a/apps/dashboard/src/components/metric/global-uptime/section.tsx +++ b/apps/dashboard/src/components/metric/global-uptime/section.tsx @@ -18,7 +18,6 @@ import { formatNumber, formatPercentage, } from "@/lib/formatter"; -import type { monitorRegions } from "@openstatus/db/src/schema/constants"; import { formatDistanceToNow } from "date-fns"; type Metric = { @@ -40,7 +39,7 @@ export function GlobalUptimeSection({ monitorId: string; jobType: "http" | "tcp"; period: "1d" | "7d" | "14d"; - regions: (typeof monitorRegions)[number][]; + regions: string[] | undefined; }) { const trpc = useTRPC(); diff --git a/apps/dashboard/src/components/nav/app-sidebar.tsx b/apps/dashboard/src/components/nav/app-sidebar.tsx index ec801f2b..178b7fd8 100644 --- a/apps/dashboard/src/components/nav/app-sidebar.tsx +++ b/apps/dashboard/src/components/nav/app-sidebar.tsx @@ -4,6 +4,7 @@ import { Activity, Bell, Cog, + Globe, LayoutGrid, PanelTop, Terminal, @@ -69,6 +70,11 @@ const data = { url: "/settings/general", icon: Cog, }, + { + name: "Private Locations", + url: "/private-locations", + icon: Globe, + }, { name: "CLI", url: "/cli", diff --git a/apps/dashboard/src/components/ui/input-group.tsx b/apps/dashboard/src/components/ui/input-group.tsx new file mode 100644 index 00000000..8253c159 --- /dev/null +++ b/apps/dashboard/src/components/ui/input-group.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; + +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
textarea]:h-auto", + + // Variants based on alignment. + "has-[>[data-align=inline-start]]:[&>input]:pl-2", + "has-[>[data-align=inline-end]]:[&>input]:pr-2", + "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3", + "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", + + // Focus state. + "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50", + + // Error state. + "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", + + className, + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50", + { + variants: { + align: { + "inline-start": + "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", + "inline-end": + "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", + "block-start": + "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5", + "block-end": + "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5", + }, + }, + defaultVariants: { + align: "inline-start", + }, + }, +); + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return; + } + e.currentTarget.parentElement?.querySelector("input")?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva( + "text-sm shadow-none flex gap-2 items-center", + { + variants: { + size: { + xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2", + sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5", + "icon-xs": + "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, + }, +); + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & + VariantProps) { + return ( +