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 (
+
+ );
+}
+
+function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ );
+}
+
+function InputGroupInput({
+ className,
+ ...props
+}: React.ComponentProps<"input">) {
+ return (
+
+ );
+}
+
+function InputGroupTextarea({
+ className,
+ ...props
+}: React.ComponentProps<"textarea">) {
+ return (
+
+ );
+}
+
+export {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupText,
+ InputGroupInput,
+ InputGroupTextarea,
+};
diff --git a/apps/dashboard/src/data/audit-logs.client.ts b/apps/dashboard/src/data/audit-logs.client.ts
index 80232a84..561e6ec0 100644
--- a/apps/dashboard/src/data/audit-logs.client.ts
+++ b/apps/dashboard/src/data/audit-logs.client.ts
@@ -1,4 +1,6 @@
import { formatMilliseconds } from "@/lib/formatter";
+import type { PrivateLocation } from "@openstatus/db/src/schema";
+import { getRegionInfo } from "@openstatus/regions";
import {
CircleAlert,
CircleCheck,
@@ -40,49 +42,58 @@ export const config = {
},
} as const;
-export const metadata = {
- region: {
- label: "Region",
- key: "region",
- unit: undefined,
- visible: () => true,
- format: (value) => String(value),
- },
- cronTimestamp: {
- label: "Timestamp",
- key: "timestamp",
- unit: undefined,
- visible: () => false,
- format: (value) => String(value),
- },
- statusCode: {
- label: "Status Code",
- key: "status",
- unit: undefined,
- visible: (_value) => typeof _value === "number" && _value !== -1,
- format: (value) => String(value),
- },
- latency: {
- label: "Latency",
- key: "latency",
- unit: "ms",
- visible: () => true,
- format: (value) => formatMilliseconds(Number(value)),
- },
- provider: {
- label: "Provider",
- key: "provider",
- unit: undefined,
- visible: () => true,
- format: (value) => String(value),
- },
-} as const satisfies Record<
- string,
- {
- label: string;
- key: string;
- unit?: string | undefined;
- visible: (value: string | number) => boolean;
- format: (value: string | number) => string;
- }
->;
+export const getMetadata = (privateLocations?: PrivateLocation[]) => {
+ return {
+ region: {
+ label: "Region",
+ key: "region",
+ unit: undefined,
+ visible: () => true,
+ format: (value) => {
+ const regionInfo = getRegionInfo(`${value}`, {
+ location: privateLocations?.find(
+ (location) => String(location.id) === String(value),
+ )?.name,
+ });
+ return `${regionInfo.location} (${regionInfo.provider})`;
+ },
+ },
+ cronTimestamp: {
+ label: "Timestamp",
+ key: "timestamp",
+ unit: undefined,
+ visible: () => false,
+ format: (value) => String(value),
+ },
+ statusCode: {
+ label: "Status Code",
+ key: "status",
+ unit: undefined,
+ visible: (_value) => typeof _value === "number" && _value !== -1,
+ format: (value) => String(value),
+ },
+ latency: {
+ label: "Latency",
+ key: "latency",
+ unit: "ms",
+ visible: () => true,
+ format: (value) => formatMilliseconds(Number(value)),
+ },
+ provider: {
+ label: "Provider",
+ key: "provider",
+ unit: undefined,
+ visible: () => true,
+ format: (value) => String(value),
+ },
+ } as const satisfies Record<
+ string,
+ {
+ label: string;
+ key: string;
+ unit?: string | undefined;
+ visible: (value: string | number) => boolean;
+ format: (value: string | number) => string;
+ }
+ >;
+};
diff --git a/apps/dashboard/src/data/metrics.client.ts b/apps/dashboard/src/data/metrics.client.ts
index 39f6205b..ead33d24 100644
--- a/apps/dashboard/src/data/metrics.client.ts
+++ b/apps/dashboard/src/data/metrics.client.ts
@@ -5,7 +5,6 @@ import { formatDateTime, formatMilliseconds } from "@/lib/formatter";
import type { RouterOutputs } from "@openstatus/api";
import { monitorRegions } from "@openstatus/db/src/schema/constants";
import type { RegionMetric } from "./region-metrics";
-import type { Region } from "./regions";
export const STATUS = ["success", "error", "degraded"] as const;
export const PERIODS = ["1d", "7d", "14d"] as const;
@@ -109,7 +108,7 @@ export function mapUptime(status: RouterOutputs["tinybird"]["uptime"]) {
*/
export function mapRegionMetrics(
timeline: RouterOutputs["tinybird"]["metricsRegions"] | undefined,
- regions: Region[],
+ regions: string[],
percentile: (typeof PERCENTILES)[number],
): RegionMetric[] {
if (!timeline)
@@ -125,14 +124,14 @@ export function mapRegionMetrics(
timestamp: number;
[key: string]: number;
}[],
- })) ?? []) as RegionMetric[];
+ })) ?? []) satisfies RegionMetric[];
type TimelineRow = (typeof timeline.data)[number];
const map = new Map<
- Region,
+ string,
{
- region: Region;
+ region: string;
p50: number;
p90: number;
p99: number;
@@ -145,10 +144,10 @@ export function mapRegionMetrics(
>();
(timeline.data as TimelineRow[])
- .filter((row) => regions.includes(row.region as Region))
+ .filter((row) => regions.includes(row.region))
.sort((a, b) => a.region.localeCompare(b.region))
.forEach((row) => {
- const region = row.region as Region;
+ const region = row.region;
const entry = map.get(region) ?? {
region,
p50: 0,
diff --git a/apps/dashboard/src/data/region-metrics.ts b/apps/dashboard/src/data/region-metrics.ts
index ba57820b..b0bf6af4 100644
--- a/apps/dashboard/src/data/region-metrics.ts
+++ b/apps/dashboard/src/data/region-metrics.ts
@@ -1,8 +1,6 @@
-import type { Region } from "./regions";
-
export const regionMetrics = [
{
- region: "ams" as const satisfies Region,
+ region: "ams",
p50: 100,
p90: 150,
p99: 200,
@@ -13,7 +11,7 @@ export const regionMetrics = [
}[],
},
{
- region: "fra" as const satisfies Region,
+ region: "fra",
p50: 110,
p90: 155,
p99: 220,
@@ -24,7 +22,7 @@ export const regionMetrics = [
}[],
},
{
- region: "gru" as const satisfies Region,
+ region: "gru",
p50: 120,
p90: 160,
p99: 230,
diff --git a/apps/dashboard/src/data/regions.ts b/apps/dashboard/src/data/regions.ts
index d3c70008..b2e0886b 100644
--- a/apps/dashboard/src/data/regions.ts
+++ b/apps/dashboard/src/data/regions.ts
@@ -376,3 +376,10 @@ export const regionColors = {
"railway_us-east4-eqdc4a": "hsl(0 0% 45.1%)",
"railway_us-west2": "hsl(0 0% 45.1%)",
} satisfies Record;
+
+export function getRegionColor(region: string) {
+ if (region in regionColors) {
+ return regionColors[region as keyof typeof regionColors];
+ }
+ return "hsl(0 0% 45.1%)";
+}
diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs
index 0b195572..b42c821f 100644
--- a/apps/docs/astro.config.mjs
+++ b/apps/docs/astro.config.mjs
@@ -96,6 +96,10 @@ export default defineConfig({
label: "How to configure a status page (beta)",
slug: "tutorial/how-to-configure-status-page",
},
+ {
+ label: "How to create a private location (beta)",
+ slug: "tutorial/how-to-create-private-location",
+ },
{
label: "Get Started with OpenStatus CLI",
slug: "tutorial/get-started-with-openstatus-cli",
diff --git a/apps/docs/src/content/docs/tutorial/how-to-create-private-location.mdx b/apps/docs/src/content/docs/tutorial/how-to-create-private-location.mdx
new file mode 100644
index 00000000..2e591668
--- /dev/null
+++ b/apps/docs/src/content/docs/tutorial/how-to-create-private-location.mdx
@@ -0,0 +1,24 @@
+---
+title: How to create a private location (beta)
+---
+
+### What are private locations?
+
+**Private locations** allow you to monitor internal applications and network resources from within your own infrastructure, rather than solely relying on public cloud-based servers.
+
+By deploying our **monitoring probes** (as containers) on your own machines inside your firewall, you can run tests against private endpoints like internal APIs or servers and get detailed *timing phases and latency information* from your specific deployment location. This enables you to monitor not just *datacenter to datacenter* (when your web services are deployed on platforms like Cloudflare, AWS, or GCP), but also from **private locations** like on-prem servers or even Raspberry Pi devices.
+
+Private locations are crucial for verifying *performance and availability* of internal systems, integrating with your development pipelines, and ensuring **security** without exposing sensitive services to the open internet.
+
+
+### How to use private locations?
+
+1. create a private location and access a generated `token`
+2. install the docker image on your server
+3. in the private location settings, choose which monitors to track (or vice versa, configure it in your monitor settings)
+4. done (within a couple of minutes, we'll check the everything)
+
+### What is missing?
+
+- Incidents will not be created (yet) if you run your monitors via privat regions. That means you should still use the openstatus regions to get alerts if a monitor is down.
+- Public monitors on status pages do not support private regions (yet).
\ No newline at end of file
diff --git a/apps/private-location/.air.toml b/apps/private-location/.air.toml
new file mode 100644
index 00000000..ba98082f
--- /dev/null
+++ b/apps/private-location/.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/private-location/.gitignore b/apps/private-location/.gitignore
new file mode 100644
index 00000000..ceeb05b4
--- /dev/null
+++ b/apps/private-location/.gitignore
@@ -0,0 +1 @@
+/tmp
diff --git a/apps/private-location/.golangci.yml b/apps/private-location/.golangci.yml
new file mode 100644
index 00000000..65109a51
--- /dev/null
+++ b/apps/private-location/.golangci.yml
@@ -0,0 +1,4 @@
+version: "2"
+
+linters:
+ default: fast
diff --git a/apps/private-location/Dockerfile b/apps/private-location/Dockerfile
new file mode 100644
index 00000000..f742ab7c
--- /dev/null
+++ b/apps/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-location ./cmd/server
+
+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-location /opt/bin/private-location
+
+ENV TZ=UTC
+ENV USER=1000
+ENV GIN_MODE=release
+
+CMD [ "/opt/bin/private-location" ]
diff --git a/apps/private-location/README.md b/apps/private-location/README.md
new file mode 100644
index 00000000..1c5de3ba
--- /dev/null
+++ b/apps/private-location/README.md
@@ -0,0 +1,4 @@
+# Private Location Orchestrator
+
+
+A server that allows private regions to register and ingest data from them.
diff --git a/apps/private-location/cmd/server/main.go b/apps/private-location/cmd/server/main.go
new file mode 100644
index 00000000..08d07a14
--- /dev/null
+++ b/apps/private-location/cmd/server/main.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/openstatushq/openstatus/apps/private-location/internal/server"
+)
+
+func gracefulShutdown(apiServer *http.Server, done chan bool) {
+ // Create context that listens for the interrupt signal from the OS.
+ ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
+
+ // Listen for the interrupt signal.
+ <-ctx.Done()
+
+ log.Println("shutting down gracefully, press Ctrl+C again to force")
+ stop() // Allow Ctrl+C to force shutdown
+
+ // The context is used to inform the server it has 5 seconds to finish
+ // the request it is currently handling
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := apiServer.Shutdown(ctx); err != nil {
+ log.Printf("Server forced to shutdown with error: %v", err)
+ }
+
+ log.Println("Server exiting")
+
+ // Notify the main goroutine that the shutdown is complete
+ done <- true
+}
+
+func main() {
+
+ server := server.NewServer()
+
+ // Create a done channel to signal when the shutdown is complete
+ done := make(chan bool, 1)
+
+ // Run graceful shutdown in a separate goroutine
+ go gracefulShutdown(server, done)
+
+ err := server.ListenAndServe()
+ if err != nil && err != http.ErrServerClosed {
+ panic(fmt.Sprintf("http server error: %s", err))
+ }
+
+ // Wait for the graceful shutdown to complete
+ <-done
+ log.Println("Graceful shutdown complete.")
+}
diff --git a/apps/private-location/fly.toml b/apps/private-location/fly.toml
new file mode 100644
index 00000000..52623abc
--- /dev/null
+++ b/apps/private-location/fly.toml
@@ -0,0 +1,42 @@
+# fly.toml app configuration file generated for openstatus-checker on 2023-11-30T20:23:20+01:00
+#
+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
+#
+
+app = "openstatus-private-location"
+primary_region = "ams"
+
+[build]
+ dockerfile = "./Dockerfile"
+
+[deploy]
+ strategy = "canary"
+
+
+[env]
+ PORT = "8080"
+
+[http_service]
+ internal_port = 8080
+ force_https = true
+ auto_stop_machines = "off"
+ auto_start_machines = false
+ processes = ["app"]
+
+[[vm]]
+ cpu_kind = "shared"
+ cpus = 1
+ memory_mb = 256
+
+
+[[http_service.checks]]
+ grace_period = "10s"
+ interval = "15s"
+ method = "GET"
+ timeout = "5s"
+ path = "/health"
+
+[http_service.concurrency]
+ type = "requests"
+ hard_limit = 1000
+ soft_limit = 500
diff --git a/apps/private-location/go.mod b/apps/private-location/go.mod
new file mode 100644
index 00000000..a098a43e
--- /dev/null
+++ b/apps/private-location/go.mod
@@ -0,0 +1,30 @@
+module github.com/openstatushq/openstatus/apps/private-location
+
+go 1.25.2
+
+require (
+ connectrpc.com/connect v1.19.1
+ github.com/go-chi/chi/v5 v5.2.3
+ github.com/go-chi/render v1.0.3
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/joho/godotenv v1.5.1
+ github.com/mattn/go-sqlite3 v1.14.22
+ github.com/openstatushq/openstatus/apps/checker v0.0.0-20251012205355-e366f661c23e
+ github.com/rs/zerolog v1.34.0
+ github.com/stretchr/testify v1.11.1
+ github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d
+ google.golang.org/protobuf v1.36.10
+)
+
+require (
+ github.com/ajg/form v1.5.1 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+ github.com/coder/websocket v1.8.12 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/apps/private-location/go.sum b/apps/private-location/go.sum
new file mode 100644
index 00000000..5f8cfd24
--- /dev/null
+++ b/apps/private-location/go.sum
@@ -0,0 +1,61 @@
+connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
+connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
+github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
+github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
+github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
+github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+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=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/openstatushq/openstatus/apps/checker v0.0.0-20251012205355-e366f661c23e h1:54C0zQNHzGszQseO2QcNzM8fL7vyAYk03pRtrJIyoV0=
+github.com/openstatushq/openstatus/apps/checker v0.0.0-20251012205355-e366f661c23e/go.mod h1:R84xAJYFys7XOZTDk/AyjJi4Ga9ovtLhJsfTLgTsYKg=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
+github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
+github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/apps/private-location/internal/database/database.go b/apps/private-location/internal/database/database.go
new file mode 100644
index 00000000..951382db
--- /dev/null
+++ b/apps/private-location/internal/database/database.go
@@ -0,0 +1,41 @@
+package database
+
+import (
+ // "database/sql"
+ "database/sql"
+ "fmt"
+
+ // "log"
+ "os"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/joho/godotenv/autoload"
+ // "github.com/tursodatabase/go-libsql"
+ _ "github.com/tursodatabase/libsql-client-go/libsql"
+)
+
+var DB *sqlx.DB
+
+var (
+ dbUrl = os.Getenv("DB_URL")
+ authToken = os.Getenv("DB_AUTH_TOKEN")
+ dbInstance *sqlx.DB
+)
+
+func New() *sqlx.DB {
+ // Reuse Connection
+ if dbInstance != nil {
+ return dbInstance
+ }
+
+ url := fmt.Sprintf("%s?auth_token=%s", dbUrl, authToken)
+ c, err := sql.Open("libsql", url)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to open db %s: %s", url, err)
+ os.Exit(1)
+ }
+
+ db := sqlx.NewDb(c, "sqlite3")
+
+ return db
+}
diff --git a/apps/private-location/internal/database/models.go b/apps/private-location/internal/database/models.go
new file mode 100644
index 00000000..0f1540ac
--- /dev/null
+++ b/apps/private-location/internal/database/models.go
@@ -0,0 +1,44 @@
+package database
+
+import "database/sql"
+
+// JobType represents the type of job for a monitor.
+type JobType string
+
+const (
+ JobTypeTCP JobType = "tcp"
+ JobTypeUDP JobType = "udp"
+ JobTypeHTTP JobType = "http"
+ JobTypeDNS JobType = "dns"
+)
+
+type Monitor struct {
+ ID int `db:"id"`
+ Active bool `db:"active"`
+ WorkspaceID int `db:"workspace_id"`
+ JobType JobType `db:"job_type"`
+ Periodicity string `db:"periodicity"`
+ URL string `db:"url"`
+ Headers string `db:"headers"`
+ Body string `db:"body"`
+ Method string `db:"method"`
+ Timeout int64 `db:"timeout"`
+ DegradedAfter sql.NullInt64 `db:"degraded_after"`
+ Assertions sql.NullString `db:"assertions"`
+ Retry int `db:"retry"`
+ FollowRedirects bool `db:"follow_redirects"`
+ OtelEndpoint sql.NullString `db:"otel_endpoint" json:"-"`
+ OtelHeaders sql.NullString `db:"otel_headers" json:"-"`
+ Name string `db:"name" json:"-"`
+ Description string `db:"description" json:"-"`
+ CreatedAt int `db:"created_at" json:"-"`
+ UpdatedAt int `db:"updated_at" json:"-"`
+ DeletedAt sql.NullInt64 `db:"deleted_at" json:"-"`
+ Regions string `db:"regions" json:"-"`
+ Status string `db:"status" json:"-"`
+ Public bool `db:"public" json:"-"`
+}
+
+type PrivateLocation struct {
+ ID int `db:"id"`
+}
diff --git a/apps/private-location/internal/models/assertions.go b/apps/private-location/internal/models/assertions.go
new file mode 100644
index 00000000..7df12059
--- /dev/null
+++ b/apps/private-location/internal/models/assertions.go
@@ -0,0 +1,76 @@
+package models
+
+import "encoding/json"
+
+type AssertionType string
+
+const (
+ AssertionHeader AssertionType = "header"
+ AssertionTextBody AssertionType = "textBody"
+ AssertionStatus AssertionType = "status"
+ AssertionJsonBody AssertionType = "jsonBody"
+)
+
+type StringComparator string
+
+func (c StringComparator) String() string {
+ return string(c)
+}
+
+func (c NumberComparator) String() string {
+ return string(c)
+}
+
+const (
+ StringContains StringComparator = "contains"
+ StringNotContains StringComparator = "not_contains"
+ StringEquals StringComparator = "eq"
+ StringNotEquals StringComparator = "not_eq"
+ StringEmpty StringComparator = "empty"
+ StringNotEmpty StringComparator = "not_empty"
+ StringGreaterThan StringComparator = "gt"
+ StringGreaterThanEqual StringComparator = "gte"
+ StringLowerThan StringComparator = "lt"
+ StringLowerThanEqual StringComparator = "lte"
+)
+
+type NumberComparator string
+
+const (
+ NumberEquals NumberComparator = "eq"
+ NumberNotEquals NumberComparator = "not_eq"
+ NumberGreaterThan NumberComparator = "gt"
+ NumberGreaterThanEqual NumberComparator = "gte"
+ NumberLowerThan NumberComparator = "lt"
+ NumberLowerThanEqual NumberComparator = "lte"
+)
+
+type Assertion struct {
+ AssertionType AssertionType `json:"type"`
+ Comparator json.RawMessage `json:"compare"`
+ RawTarget json.RawMessage `json:"target"`
+}
+
+type StatusTarget struct {
+ AssertionType AssertionType `json:"type"`
+ Comparator NumberComparator `json:"compare"`
+ Target int64 `json:"target"`
+}
+
+type HeaderTarget struct {
+ AssertionType AssertionType `json:"type"`
+ Comparator StringComparator `json:"compare"`
+ Target string `json:"target"`
+ Key string `json:"key"`
+}
+
+type StringTargetType struct {
+ Comparator StringComparator `json:"compare"`
+ Target string `json:"target"`
+}
+
+type BodyString struct {
+ AssertionType AssertionType `json:"type"`
+ Comparator StringComparator `json:"compare"`
+ Target string `json:"target"`
+}
diff --git a/apps/private-location/internal/server/db_testdata b/apps/private-location/internal/server/db_testdata
new file mode 100644
index 00000000..764739c7
--- /dev/null
+++ b/apps/private-location/internal/server/db_testdata
@@ -0,0 +1,465 @@
+DROP TABLE IF EXISTS "__drizzle_migrations";
+CREATE TABLE "__drizzle_migrations" (
+ id SERIAL PRIMARY KEY,
+ hash text NOT NULL,
+ created_at numeric
+ );
+
+DROP TABLE IF EXISTS "page";
+CREATE TABLE `page` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `workspace_id` integer NOT NULL,
+ `title` text NOT NULL,
+ `description` text NOT NULL,
+ `icon` text(256),
+ `slug` text(256) NOT NULL,
+ `custom_domain` text(256) NOT NULL,
+ `published` integer DEFAULT false,
+ "created_at" integer DEFAULT (strftime('%s', 'now')), `updated_at` integer, `password` text(256), `password_protected` integer DEFAULT false, `show_monitor_values` integer DEFAULT true, `force_theme` text DEFAULT 'system' NOT NULL, `legacy_page` integer DEFAULT true NOT NULL, `configuration` text, `homepage_url` text(256), `contact_url` text(256),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "monitors_to_pages";
+CREATE TABLE `monitors_to_pages` (
+ `monitor_id` integer NOT NULL,
+ `page_id` integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')), `order` integer DEFAULT 0,
+ PRIMARY KEY(`monitor_id`, `page_id`),
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "user";
+CREATE TABLE `user` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `tenant_id` text(256),
+ "created_at" integer DEFAULT (strftime('%s', 'now'))
+, `first_name` text DEFAULT '', `last_name` text DEFAULT '', `email` text DEFAULT '', `photo_url` text DEFAULT '', `updated_at` integer, `name` text, `emailVerified` integer);
+
+DROP TABLE IF EXISTS "users_to_workspaces";
+CREATE TABLE `users_to_workspaces` (
+ `user_id` integer NOT NULL,
+ `workspace_id` integer NOT NULL, `role` text DEFAULT 'owner' NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY(`user_id`, `workspace_id`),
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "workspace";
+CREATE TABLE `workspace` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `slug` text NOT NULL,
+ `stripe_id` text(256),
+ `name` text,
+ "created_at" integer DEFAULT (strftime('%s', 'now'))
+, `updated_at` integer, `subscription_id` text, `plan` text(3), `ends_at` integer, `paid_until` integer, `dsn` text, `limits` text DEFAULT '{}' NOT NULL);
+
+DROP TABLE IF EXISTS "status_report_update";
+CREATE TABLE "status_report_update" (
+ `id` integer PRIMARY KEY NOT NULL,
+ "status" text NOT NULL,
+ `date` integer NOT NULL,
+ `message` text NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ "status_report_id" integer NOT NULL,
+ FOREIGN KEY ("status_report_id") REFERENCES "status_report"(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "status_report_to_monitors";
+CREATE TABLE "status_report_to_monitors" (
+ `monitor_id` integer NOT NULL,
+ "status_report_id" integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY("status_report_id", `monitor_id`),
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY ("status_report_id") REFERENCES "status_report"(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "monitor";
+CREATE TABLE "monitor" (
+ `id` integer PRIMARY KEY NOT NULL,
+ `job_type` text(3) DEFAULT 'other' NOT NULL,
+ `periodicity` text(6) DEFAULT 'other' NOT NULL,
+ `active` integer DEFAULT false,
+ `url` text(512) NOT NULL,
+ `name` text(256) DEFAULT '' NOT NULL,
+ `description` text DEFAULT '' NOT NULL,
+ `workspace_id` integer,
+ `headers` text DEFAULT '',
+ `body` text DEFAULT '',
+ `method` text(5) DEFAULT 'GET',
+ `created_at` integer DEFAULT (strftime('%s', 'now')), `regions` text DEFAULT '' NOT NULL, `updated_at` integer, `status` text(2) DEFAULT 'active' NOT NULL, `assertions` text, `deleted_at` integer, `public` integer DEFAULT false, `timeout` integer DEFAULT 45000 NOT NULL, `degraded_after` integer, `otel_endpoint` text, `otel_headers` text, `retry` integer DEFAULT 3, `follow_redirects` integer DEFAULT true,
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "integration";
+CREATE TABLE `integration` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `name` text(256) NOT NULL,
+ `workspace_id` integer,
+ `credential` text,
+ `external_id` text NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ `data` text NOT NULL,
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "notification";
+CREATE TABLE `notification` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `provider` text NOT NULL,
+ `data` text DEFAULT '{}',
+ `workspace_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "notifications_to_monitors";
+CREATE TABLE `notifications_to_monitors` (
+ `monitor_id` integer NOT NULL,
+ `notification_id` integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY(`monitor_id`, `notification_id`),
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "status_reports_to_pages";
+CREATE TABLE "status_reports_to_pages" (
+ `page_id` integer NOT NULL,
+ "status_report_id" integer NOT NULL, `created_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY("status_report_id", `page_id`),
+ FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY ("status_report_id") REFERENCES "status_report"(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "monitor_status";
+CREATE TABLE `monitor_status` (
+ `monitor_id` integer NOT NULL,
+ `region` text DEFAULT '' NOT NULL,
+ `status` text DEFAULT 'active' NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY(`monitor_id`, `region`),
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "invitation";
+CREATE TABLE `invitation` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `email` text NOT NULL,
+ `role` text DEFAULT 'member' NOT NULL,
+ `workspace_id` integer NOT NULL,
+ `token` text NOT NULL,
+ `expires_at` integer NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `accepted_at` integer
+);
+
+DROP TABLE IF EXISTS "incident";
+CREATE TABLE "incident" (
+ `id` integer PRIMARY KEY NOT NULL,
+ `title` text DEFAULT '' NOT NULL,
+ `summary` text DEFAULT '' NOT NULL,
+ `status` text DEFAULT 'triage' NOT NULL,
+ `monitor_id` integer,
+ `workspace_id` integer,
+ `started_at` integer DEFAULT (strftime('%s', 'now')) NOT NULL,
+ `acknowledged_at` integer,
+ `acknowledged_by` integer,
+ `resolved_at` integer,
+ `resolved_by` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')), `auto_resolved` integer DEFAULT false, `incident_screenshot_url` text, `recovery_screenshot_url` text,
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE set default,
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`acknowledged_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`resolved_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "monitor_tag";
+CREATE TABLE `monitor_tag` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `workspace_id` integer NOT NULL,
+ `name` text NOT NULL,
+ `color` text NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "monitor_tag_to_monitor";
+CREATE TABLE `monitor_tag_to_monitor` (
+ `monitor_id` integer NOT NULL,
+ `monitor_tag_id` integer NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY(`monitor_id`, `monitor_tag_id`),
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`monitor_tag_id`) REFERENCES `monitor_tag`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "account";
+CREATE TABLE `account` (
+ `user_id` integer NOT NULL,
+ `type` text NOT NULL,
+ `provider` text NOT NULL,
+ `provider_account_id` text NOT NULL,
+ `refresh_token` text,
+ `access_token` text,
+ `expires_at` integer,
+ `token_type` text,
+ `scope` text,
+ `id_token` text,
+ `session_state` text,
+ PRIMARY KEY(`provider`, `provider_account_id`),
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "session";
+CREATE TABLE `session` (
+ `session_token` text PRIMARY KEY NOT NULL,
+ `user_id` integer NOT NULL,
+ `expires` integer NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "verification_token";
+CREATE TABLE `verification_token` (
+ `identifier` text NOT NULL,
+ `token` text NOT NULL,
+ `expires` integer NOT NULL,
+ PRIMARY KEY(`identifier`, `token`)
+);
+
+DROP TABLE IF EXISTS "application";
+CREATE TABLE `application` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `name` text,
+ `dsn` text,
+ `workspace_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "maintenance_to_monitor";
+CREATE TABLE `maintenance_to_monitor` (
+ `monitor_id` integer NOT NULL,
+ `maintenance_id` integer NOT NULL,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ PRIMARY KEY(`maintenance_id`, `monitor_id`),
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`maintenance_id`) REFERENCES `maintenance`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "check";
+CREATE TABLE `check` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `regions` text DEFAULT '' NOT NULL,
+ `url` text(4096) NOT NULL,
+ `headers` text DEFAULT '',
+ `body` text DEFAULT '',
+ `method` text DEFAULT 'GET',
+ `count_requests` integer DEFAULT 1,
+ `workspace_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+
+
+DROP TABLE IF EXISTS "monitor_run";
+CREATE TABLE `monitor_run` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `workspace_id` integer,
+ `monitor_id` integer,
+ `runned_at` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "page_subscriber";
+CREATE TABLE "page_subscriber" (
+ `id` integer PRIMARY KEY NOT NULL,
+ `email` text NOT NULL,
+ `page_id` integer NOT NULL,
+ `token` text,
+ `accepted_at` integer,
+ `expires_at` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "status_report";
+CREATE TABLE "status_report" (
+ `id` integer PRIMARY KEY NOT NULL,
+ `status` text NOT NULL,
+ `title` text(256) NOT NULL,
+ `workspace_id` integer,
+ `page_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "notification_trigger";
+CREATE TABLE "notification_trigger" (
+ `id` integer PRIMARY KEY NOT NULL,
+ `monitor_id` integer,
+ `notification_id` integer,
+ `cron_timestamp` integer NOT NULL,
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`notification_id`) REFERENCES `notification`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "maintenance";
+CREATE TABLE "maintenance" (
+ `id` integer PRIMARY KEY NOT NULL,
+ `title` text(256) NOT NULL,
+ `message` text NOT NULL,
+ `from` integer NOT NULL,
+ `to` integer NOT NULL,
+ `workspace_id` integer,
+ `page_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade
+);
+
+DROP TABLE IF EXISTS "private_location";
+CREATE TABLE `private_location` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `token` text NOT NULL,
+ `last_seen_at` integer,
+ `workspace_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+DROP TABLE IF EXISTS "private_location_to_monitor";
+CREATE TABLE `private_location_to_monitor` (
+ `private_location_id` integer,
+ `monitor_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `deleted_at` integer,
+ FOREIGN KEY (`private_location_id`) REFERENCES `private_location`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action
+);
+
+
+INSERT INTO "__drizzle_migrations" ("id", "hash", "created_at") VALUES
+(NULL, 'ea497587bb639bbeae27f3f644634b7429f37df241c999e22f3acbf3cce74ec9', '1690309905039'),
+(NULL, '680e79fc5537135bcfa4da88cc0c06a7a9eaf810ab87f8ae06ef67f7b4802fad', '1690892003254'),
+(NULL, 'b8ae7d5887e4cd8416a443f193753dabc6bc1ee76a6cbcfd4b338fbc6d3d10e7', '1691573899721'),
+(NULL, '790cf15e409b0c02f0e164824289aaacacb7aee21f5e84ab4f5a48df4b46e528', '1691614487733'),
+(NULL, 'ac5db9b31935382412791565e1c11ce1c99e69bd6f826da9ff08fb4ea64acc67', '1691850907670'),
+(NULL, '4cc5cc57b2e087276e6283cbd639e2ccd27c800eec6def5bb3ff7ea3ee3d9ba9', '1691930414569'),
+(NULL, 'cc99f49622240771a187a79a7c5646ed7f1e6e86b6eefcc04e9a6be78e57a5b0', '1692646649111'),
+(NULL, '49e497500b2fdb01624090863808ab15db1e50d4bc393c1e6e820960cb070d60', '1694362217174'),
+(NULL, 'a76eb5c9a12c6828bdef95e6ca65483055e7dabdf94f0b95fdae437c811b6b55', '1695756345957'),
+(NULL, '223292dcce0a81148dfbf7338ed4fe5878503d6bf785e1a13042b7c7b7b5bf24', '1697285841283'),
+(NULL, '5cc395568f9f61ebf4b57f69fd6397da2661062db813a85c5433d56841cecc9c', '1700586221141'),
+(NULL, '4ceefb80e855b8fde6c430758d9b557cef09c42afca4575c8fe4236628112d33', '1701100570578'),
+(NULL, '4c8d34ebf56874f41a1b22cb93db50e022233ff53f763eb73a73d2e01223fa39', '1701713135829'),
+(NULL, '4f47a6efc84f60e41437134c6eec5b2a3be68aae2a0525f2109b61f7473bb5ea', '1702144660818'),
+(NULL, '6f1b9eac8a3c7cf72703171402a9f42ea7523c091e393b3972fd231ab6eac6f9', '1702227904130'),
+(NULL, '4ae3b4c679065c4ca450126e14a73dbb3d21d71b7b1de2be182c32500b5c16ea', '1705856545397'),
+(NULL, '04e2ca02dbf77bce1755dc28e624d1620d662bec94373cea08983f4801fe778d', '1706111184826'),
+(NULL, 'd3c0f8670dd8e3666b3e0bd3ef1d75aec6b593c11d35e5a75bf58abe6208c642', '1707411900987'),
+(NULL, 'a028a05328ef4480fb5d79caed96da17b68793f7c9af037dbe8bb77ce42f5e36', '1707770189561'),
+(NULL, '826d9207a73609ff0f15acb2109fed80a253beb826836a93e122157584f9ca35', '1707899175705'),
+(NULL, '0055fd4b62b8b1c7029ed978cdd2f946084acae39966cb8cf85c26923a87d1cc', '1707905605592'),
+(NULL, 'af0035a9de14837b62dddba9e40bb85bf249a0c75683867be2a2d6f70f28da6d', '1710677383007'),
+(NULL, '498de5adf9c0ea5ed4c3d57b6a0905b709a58550e79c157909f13e1269896c5a', '1711307113089'),
+(NULL, 'ce825562d32c32c28da76e2d06b5e76f4c1603d178ce82ff97e1436819f3273c', '1712311348272'),
+(NULL, '45fc9d668e1a0d69701267444ca79da8e392180635761f24e41d9a7ac30a6d8c', '1712354121499'),
+(NULL, 'e9a11163fbaa7bdb2e2350265cc3e260bdb24ae1eb42d975fbf03cba7b3c8ce7', '1713095971713'),
+(NULL, '62f1cd998d6258df5c2a89222f5b71082d575b474f6d86b75fb4bf3aefe1d063', '1713384976187'),
+(NULL, '4c0d7cd418b66257e0a49454c54e8e65c388c5a246b3f5a51df0618da1faa67d', '1714586658374'),
+(NULL, 'd785a89fd7aa7bad6dbae4db4a3daaaa26c5f14374ed045cfd22420453cc4cea', '1715173356076'),
+(NULL, '9216b9d717de25f7b615f625659b7acfbbb09adacd3b121fa4f05ec0796fbd3c', '1716215342026'),
+(NULL, 'a9ced3ac1a3f9e8de9d8240ccdeed9d9e9c80b1bb6caefea59a7dbf7cf411fa7', '1716364430118'),
+(NULL, '9d0ae1d1e4bc742555e5ae5d990b62d23bb3dcefd7f62079df0bfa908f74133b', '1717837961923'),
+(NULL, '698cdffe2cad7d5b65190d09cb8f2b2ba906b1218fedb1a727fb72049090dfb6', '1718027484219'),
+(NULL, '6255470457dc29e4e1f7b8aa84f349a68b04ba3d51b9380006ddac53d5ce90a2', '1719740057514'),
+(NULL, 'bbf2f3122e76c5cbd3b76bc36e97d8f8efdddfab25c98c16f8024154e9bb2609', '1720727898360'),
+(NULL, '812b9def5df2ffe21b91680764590eda65e5ac4674500966d921d9e8627ca2f0', '1721159796428'),
+(NULL, '56181582f13b5eb66f2598a28fc7800ec6a0b9dd31d74c99cea1299ea7321917', '1723459608109'),
+(NULL, '82e57027c88612343af361f30edb148f2fca91919d6fa44048e992462d71163d', '1729533101998'),
+(NULL, '215e2ab2354eaa50923566a10a99e79f8d8514d7e3d4ca5c5720a9f5d16714ee', '1729579461221'),
+(NULL, 'fca63d63ad18ce6864418f1597b7bb0013ab2a3e0b4c681326ee4b257de2eb66', '1739193014150'),
+(NULL, '42d3cbf03d76541be2bdd9036e2f97864aa2807fe625f3b4c2b4f6440db60bd4', '1740684132626'),
+(NULL, '1a8b01776fbc88eaad155e2d0d2c69fc41d970411193ddd97806d70e1c66248b', '1741936835660'),
+(NULL, '427ca6a7f11dce1f4c0a20344d32af5f3f146a128adce1e87c7f76c80ab080fd', '1747410497521'),
+(NULL, '150ecc57f8a96341713848b1eec1293e713289b8b87854067d543fce3ff03849', '1747908803707'),
+(NULL, '8980cf55bf7b9b7f9a2cb00e5f21be8454260dd2826644b7683aaad65ff40df6', '1753730490635'),
+(NULL, 'bd5e4767fcfcd79179bb9ac9576b58a0c51c0a3af65fc0140e53f57c40e56a9c', '1756185045968'),
+(NULL, 'c7180363acc6879b749b2e9c34d3b9ac012f13aed0c62bf7ed2104e936f72085', '1757580216081'),
+(NULL, '0b90478b428afe479c6da99669ba33dd78ca2b8b5039ca2305bd9771213f4089', '1757840904190'),
+(NULL, 'b2c7b149f424a30adbf9dbe4c1db363e85d5dfe9aca3845969b14238c1117178', '1759865914553');
+
+INSERT INTO "page" ("id", "workspace_id", "title", "description", "icon", "slug", "custom_domain", "published", "created_at", "updated_at", "password", "password_protected", "show_monitor_values", "force_theme", "legacy_page", "configuration", "homepage_url", "contact_url") VALUES
+('1', '1', 'Test Page', 'hello', 'https://www.openstatus.dev/favicon.ico', 'status', '', '1', '1760358329', '1760358329', NULL, '0', '1', 'system', '1', NULL, NULL, NULL);
+
+INSERT INTO "monitors_to_pages" ("monitor_id", "page_id", "created_at", "order") VALUES
+('1', '1', '1760358329', '0');
+
+INSERT INTO "user" ("id", "tenant_id", "created_at", "first_name", "last_name", "email", "photo_url", "updated_at", "name", "emailVerified") VALUES
+('1', '1', '1760358329', 'Speed', 'Matters', 'ping@openstatus.dev', '', '1760358329', NULL, NULL);
+
+INSERT INTO "users_to_workspaces" ("user_id", "workspace_id", "role", "created_at") VALUES
+('1', '1', 'member', '1760358329');
+
+INSERT INTO "workspace" ("id", "slug", "stripe_id", "name", "created_at", "updated_at", "subscription_id", "plan", "ends_at", "paid_until", "dsn", "limits") VALUES
+('1', 'love-openstatus', 'stripeId1', 'test', '1760358329', '1760358329', 'subscriptionId', 'team', NULL, NULL, NULL, '{"monitors":50,"synthetic-checks":150000,"periodicity":["30s","1m","5m","10m","30m","1h"],"multi-region":true,"max-regions":35,"data-retention":"24 months","status-pages":20,"maintenance":true,"status-subscribers":true,"custom-domain":true,"password-protection":true,"white-label":true,"notifications":true,"sms":true,"pagerduty":true,"notification-channels":50,"members":"Unlimited","audit-log":true,"regions":["ams","arn","atl","bog","bom","bos","cdg","den","dfw","ewr","eze","fra","gdl","gig","gru","hkg","iad","jnb","lax","lhr","mad","mia","nrt","ord","otp","phx","qro","scl","sea","sin","sjc","syd","waw","yul","yyz"]}'),
+('2', 'test2', 'stripeId2', 'test2', '1760358329', '1760358329', 'subscriptionId2', 'free', NULL, NULL, NULL, '{}'),
+('3', 'test3', 'stripeId3', 'test3', '1760358329', '1760358329', 'subscriptionId3', 'team', NULL, NULL, NULL, '{}');
+
+INSERT INTO "status_report_update" ("id", "status", "date", "message", "created_at", "updated_at", "status_report_id") VALUES
+('1', 'investigating', '1760358329', 'Message', '1760358329', '1760358329', '1'),
+('2', 'investigating', '1760358329', 'Message', '1760358329', '1760358329', '2'),
+('3', 'monitoring', '1760358329', 'test', '1760358329', '1760358329', '1');
+
+INSERT INTO "status_report_to_monitors" ("monitor_id", "status_report_id", "created_at") VALUES
+('1', '2', '1760358329'),
+('2', '2', '1760358329');
+
+INSERT INTO "monitor" ("id", "job_type", "periodicity", "active", "url", "name", "description", "workspace_id", "headers", "body", "method", "created_at", "regions", "updated_at", "status", "assertions", "deleted_at", "public", "timeout", "degraded_after", "otel_endpoint", "otel_headers", "retry", "follow_redirects") VALUES
+('1', 'http', '1m', '1', 'https://www.openstatus.dev', 'OpenStatus', 'OpenStatus website', '1', '[{"key":"key", "value":"value"}]', '{"hello":"world"}', 'POST', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '0', '45000', NULL, NULL, NULL, '3', '1'),
+('2', 'http', '10m', '0', 'https://www.google.com', '', '', '1', '', '', 'GET', '1760358329', 'gru', '1760358329', 'active', NULL, NULL, '1', '45000', NULL, NULL, NULL, '3', '1'),
+('3', 'http', '1m', '1', 'https://www.openstatus.dev', 'OpenStatus', 'OpenStatus website', '1', '[{"key":"key", "value":"value"}]', '{"hello":"world"}', 'GET', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '0', '45000', NULL, NULL, NULL, '3', '1'),
+('4', 'http', '10m', '1', 'https://www.google.com', '', '', '1', '', '', 'GET', '1760358329', 'gru', '1760358329', 'active', NULL, NULL, '1', '45000', NULL, 'https://otel.com:4337', '[{"key":"Authorization","value":"Basic"}]', '3', '1'),
+('5', 'http', '10m', '1', 'https://openstat.us', '', '', '3', '', '', 'GET', '1760358329', 'ams', '1760358329', 'active', NULL, NULL, '1', '45000', NULL, NULL, NULL, '3', '1');
+
+INSERT INTO "notification" ("id", "name", "provider", "data", "workspace_id", "created_at", "updated_at") VALUES
+('1', 'sample test notification', 'email', '{"email":"ping@openstatus.dev"}', '1', '1760358329', '1760358329');
+
+INSERT INTO "notifications_to_monitors" ("monitor_id", "notification_id", "created_at") VALUES
+('1', '1', '1760358329');
+
+INSERT INTO "incident" ("id", "title", "summary", "status", "monitor_id", "workspace_id", "started_at", "acknowledged_at", "acknowledged_by", "resolved_at", "resolved_by", "created_at", "updated_at", "auto_resolved", "incident_screenshot_url", "recovery_screenshot_url") VALUES
+('1', '', '', 'triage', '1', '1', '1760358329', NULL, NULL, NULL, NULL, '1760358329', '1760358329', '0', NULL, NULL),
+('2', '', '', 'triage', '1', '1', '1760358330', NULL, NULL, NULL, NULL, '1760358329', '1760358329', '0', NULL, NULL);
+
+INSERT INTO "maintenance_to_monitor" ("monitor_id", "maintenance_id", "created_at") VALUES
+('1', '1', '1760358329');
+
+INSERT INTO "status_report" ("id", "status", "title", "workspace_id", "page_id", "created_at", "updated_at") VALUES
+('1', 'monitoring', 'Test Status Report', '1', '1', '1760358329', '1760358329'),
+('2', 'investigating', 'Test Status Report', '1', '1', '1760358329', '1760358329');
+
+INSERT INTO "maintenance" ("id", "title", "message", "from", "to", "workspace_id", "page_id", "created_at", "updated_at") VALUES
+('1', 'Test Maintenance', 'Test message', '1760358329', '1760358330', '1', '1', '1760358329', '1760358329');
+
+INSERT INTO "private_location" ("id", "name", "token", "last_seen_at", "workspace_id", "created_at", "updated_at") VALUES
+('1', 'My Home', 'my-secret-key', NULL, '3', '1760358329', '1760358329');
+
+INSERT INTO "private_location_to_monitor" ("private_location_id", "monitor_id", "created_at", "deleted_at") VALUES
+('1', '5', '1760358329', NULL);
diff --git a/apps/private-location/internal/server/ingest_http.go b/apps/private-location/internal/server/ingest_http.go
new file mode 100644
index 00000000..fcb1cefa
--- /dev/null
+++ b/apps/private-location/internal/server/ingest_http.go
@@ -0,0 +1,92 @@
+package server
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/database"
+ private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+ "github.com/rs/zerolog/log"
+)
+
+type PingData struct {
+ ID string `json:"id"`
+ WorkspaceID string `json:"workspaceId"`
+ MonitorID string `json:"monitorId"`
+ URL string `json:"url"`
+ Method string `json:"method"`
+ Region string `json:"region"`
+ Message string `json:"message,omitempty"`
+ Timing string `json:"timing,omitempty"`
+ Headers string `json:"headers,omitempty"`
+ Assertions string `json:"assertions"`
+ Body string `json:"body,omitempty"`
+ Trigger string `json:"trigger,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"`
+}
+
+func (h *privateLocationHandler) IngestHTTP(ctx context.Context, req *connect.Request[private_locationv1.IngestHTTPRequest]) (*connect.Response[private_locationv1.IngestHTTPResponse], error) {
+ token := req.Header().Get("openstatus-token")
+ if token == "" {
+ return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing token"))
+ }
+
+ dataSourceName := "ping_response__v8"
+
+ var monitors database.Monitor
+ err := h.db.Get(&monitors, "SELECT monitor.* FROM monitor JOIN private_location_to_monitor a ON monitor.id = a.monitor_id JOIN private_location b ON a.private_location_id = b.id WHERE b.token = ? AND monitor.deleted_at IS NULL and monitor.id = ?", token, req.Msg.MonitorId)
+
+ if err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("Failed to get monitors")
+
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+
+ var region database.PrivateLocation
+ err = h.db.Get(®ion, "SELECT private_location.id FROM private_location join private_location_to_monitor a ON private_location.id = a.private_location_id WHERE a.monitor_id = ? AND private_location.token = ?", monitors.ID, token)
+
+ if err != nil {
+
+ log.Ctx(ctx).Error().Err(err).Msg("Failed to get private location")
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+
+ data := PingData{
+ ID: req.Msg.Id,
+ Latency: req.Msg.Latency,
+ StatusCode: int(req.Msg.StatusCode),
+ MonitorID: req.Msg.MonitorId,
+ Region: strconv.Itoa(region.ID),
+ WorkspaceID: strconv.Itoa(monitors.WorkspaceID),
+ Timestamp: req.Msg.Timestamp,
+ CronTimestamp: req.Msg.CronTimestamp,
+ URL: monitors.URL,
+ Method: monitors.Method,
+ Timing: req.Msg.Timing,
+ Headers: req.Msg.Headers,
+ Body: req.Msg.Body,
+ Trigger: "cron",
+ RequestStatus: req.Msg.RequestStatus,
+ Assertions: monitors.Assertions.String,
+ }
+ if err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("failed to send event to tinybird")
+ }
+ _, err = h.db.NamedExec("UPDATE private_location SET last_seen_at = :last_seen_at WHERE id = :id", map[string]any{
+ "last_seen_at": time.Now().Unix(),
+ "id": region.ID,
+ })
+ if err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("failed to update private location")
+ }
+
+ return connect.NewResponse(&private_locationv1.IngestHTTPResponse{}), nil
+}
diff --git a/apps/private-location/internal/server/ingest_http_test.go b/apps/private-location/internal/server/ingest_http_test.go
new file mode 100644
index 00000000..a851f018
--- /dev/null
+++ b/apps/private-location/internal/server/ingest_http_test.go
@@ -0,0 +1,129 @@
+package server_test
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "os"
+ "testing"
+
+ "connectrpc.com/connect"
+ "github.com/jmoiron/sqlx"
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/server"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/tinybird"
+ private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+)
+
+func testDB() *sqlx.DB {
+
+ f, err := os.CreateTemp("", "db")
+ if err != nil {
+ log.Fatalln(err)
+ }
+ db, err := sqlx.Connect("sqlite3", f.Name())
+ if err != nil {
+ log.Fatalln(err)
+ }
+ dat, err := os.ReadFile("./db_testdata")
+ db.MustExec(string(dat))
+
+ return db
+}
+
+type interceptorHTTPClient struct {
+ f func(req *http.Request) (*http.Response, error)
+}
+
+func (i *interceptorHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) {
+ return i.f(req)
+}
+
+func (i *interceptorHTTPClient) GetHTTPClient() *http.Client {
+ return &http.Client{
+ Transport: i,
+ }
+}
+
+func getTBClient(ctx context.Context) tinybird.Client {
+ interceptor := &interceptorHTTPClient{
+ f: func(req *http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: http.StatusAccepted,
+ }, nil
+ },
+ }
+
+ client := tinybird.NewClient(interceptor.GetHTTPClient(), "apiKey")
+ return client
+}
+
+func TestIngestHTTP_Unauthenticated(t *testing.T) {
+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
+
+ req := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})
+ // No token header
+ resp, err := h.IngestHTTP(context.Background(), req)
+ if err == nil {
+ t.Fatalf("expected error for missing token, got nil")
+ }
+ if connect.CodeOf(err) != connect.CodeUnauthenticated {
+ t.Errorf("expected unauthenticated code, got %v", connect.CodeOf(err))
+ }
+ if resp != nil {
+ t.Errorf("expected nil response, got %v", resp)
+ }
+}
+
+func TestIngestHTTP_DBError(t *testing.T) {
+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
+
+ req := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})
+ req.Header().Set("openstatus-token", "token123")
+ req.Msg.Id = "monitor1"
+ resp, err := h.IngestHTTP(context.Background(), req)
+ if err == nil {
+ t.Fatalf("expected error for db failure, got nil")
+ }
+ if connect.CodeOf(err) != connect.CodeInternal {
+ t.Errorf("expected internal code, got %v", connect.CodeOf(err))
+ }
+ if resp != nil {
+ t.Errorf("expected nil response, got %v", resp)
+ }
+}
+
+func TestIngestHTTP_MonitorNotExist(t *testing.T) {
+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
+
+ req := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})
+ req.Header().Set("openstatus-token", "my-secret-key")
+ req.Msg.Id = "monitor1"
+ resp, err := h.IngestHTTP(context.Background(), req)
+ if err == nil {
+ t.Fatalf("expected error for db failure, got nil")
+ }
+ if connect.CodeOf(err) != connect.CodeInternal {
+ t.Errorf("expected internal code, got %v", connect.CodeOf(err))
+ }
+ if resp != nil {
+ t.Errorf("expected nil response, got %v", resp)
+ }
+}
+
+func TestIngestHTTP_MonitorExist(t *testing.T) {
+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
+
+ req := connect.NewRequest(&private_locationv1.IngestHTTPRequest{})
+ req.Header().Set("openstatus-token", "my-secret-key")
+ req.Msg.Id = "monitor1"
+ req.Msg.MonitorId = "5"
+ resp, err := h.IngestHTTP(context.Background(), req)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if resp == nil {
+ t.Errorf("expected not response, got %v", resp)
+ }
+}
diff --git a/apps/private-location/internal/server/ingest_tcp.go b/apps/private-location/internal/server/ingest_tcp.go
new file mode 100644
index 00000000..538cdc79
--- /dev/null
+++ b/apps/private-location/internal/server/ingest_tcp.go
@@ -0,0 +1,82 @@
+package server
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/database"
+ private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+ "github.com/rs/zerolog/log"
+)
+
+type TCPData struct {
+ ID string `json:"id"`
+ Timing string `json:"timing"`
+ ErrorMessage string `json:"errorMessage"`
+ Region string `json:"region"`
+ Trigger string `json:"trigger"`
+ URI string `json:"uri"`
+ RequestStatus string `json:"requestStatus,omitempty"`
+
+ RequestId int64 `json:"requestId,omitempty"`
+ WorkspaceID int64 `json:"workspaceId"`
+ MonitorID int64 `json:"monitorId"`
+ Timestamp int64 `json:"timestamp"`
+ Latency int64 `json:"latency"`
+ CronTimestamp int64 `json:"cronTimestamp"`
+
+ Error uint8 `json:"error"`
+}
+
+func (h *privateLocationHandler) IngestTCP(ctx context.Context, req *connect.Request[private_locationv1.IngestTCPRequest]) (*connect.Response[private_locationv1.IngestTCPResponse], error) {
+ token := req.Header().Get("openstatus-token")
+ if token == "" {
+ return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing token"))
+ }
+
+ dataSourceName := "tcp_response__v0"
+
+ var monitors database.Monitor
+ err := h.db.Get(&monitors, "SELECT monitor.* FROM monitor JOIN private_location_to_monitor a ON monitor.id = a.monitor_id JOIN private_location b ON a.private_location_id = b.id WHERE b.token = ? AND monitor.deleted_at IS NULL and monitor.id = ?", token, req.Msg.Id)
+
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+
+ var region database.PrivateLocation
+ err = h.db.Get(®ion, "SELECT private_location.id FROM private_location join private_location_to_monitor a ON private_location.id = a.private_location_id WHERE a.monitor_id = ? and private_location.token = ?", monitors.ID, token)
+
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+
+ data := TCPData{
+ ID: req.Msg.Id,
+ WorkspaceID: int64(monitors.WorkspaceID),
+ Timestamp: req.Msg.Timestamp,
+ Error: uint8(req.Msg.Error),
+ // ErrorMessage: req.Msg.ErrorMessage,
+ Region: strconv.Itoa(region.ID),
+ MonitorID: int64(monitors.ID),
+ Timing: req.Msg.Timing,
+ Latency: req.Msg.Latency,
+ CronTimestamp: req.Msg.CronTimestamp,
+ Trigger: "cron",
+ URI: req.Msg.Uri,
+ RequestStatus: req.Msg.RequestStatus,
+ }
+ if err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("failed to send event to tinybird")
+ }
+ _, err = h.db.NamedExec("UPDATE private_location SET last_seen_at = :last_seen_at WHERE id = :id", map[string]any{
+ "last_seen_at": time.Now().Unix(),
+ "id": region.ID,
+ })
+ if err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("failed to update private location")
+ }
+ return connect.NewResponse(&private_locationv1.IngestTCPResponse{}), nil
+}
diff --git a/apps/private-location/internal/server/ingest_tcp_test.go b/apps/private-location/internal/server/ingest_tcp_test.go
new file mode 100644
index 00000000..11f693b0
--- /dev/null
+++ b/apps/private-location/internal/server/ingest_tcp_test.go
@@ -0,0 +1,48 @@
+package server_test
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "connectrpc.com/connect"
+
+ "github.com/openstatushq/openstatus/apps/private-location/internal/server"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/tinybird"
+ private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+)
+
+func TestIngestTCP_Unauthenticated(t *testing.T) {
+ h := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, ""))
+
+ req := connect.NewRequest(&private_locationv1.IngestTCPRequest{})
+ // No token header
+ resp, err := h.IngestTCP(context.Background(), req)
+ if err == nil {
+ t.Fatalf("expected error for missing token, got nil")
+ }
+ if connect.CodeOf(err) != connect.CodeUnauthenticated {
+ t.Errorf("expected unauthenticated code, got %v", connect.CodeOf(err))
+ }
+ if resp != nil {
+ t.Errorf("expected nil response, got %v", resp)
+ }
+}
+
+func TestIngestTCP_DBError(t *testing.T) {
+ h := server.NewPrivateLocationServer(testDB(), tinybird.NewClient(http.DefaultClient, ""))
+
+ req := connect.NewRequest(&private_locationv1.IngestTCPRequest{})
+ req.Header().Set("openstatus-token", "token123")
+ req.Msg.Id = "monitor1"
+ resp, err := h.IngestTCP(context.Background(), req)
+ if err == nil {
+ t.Fatalf("expected error for db failure, got nil")
+ }
+ if connect.CodeOf(err) != connect.CodeInternal {
+ t.Errorf("expected internal code, got %v", connect.CodeOf(err))
+ }
+ if resp != nil {
+ t.Errorf("expected nil response, got %v", resp)
+ }
+}
diff --git a/apps/private-location/internal/server/monitors.go b/apps/private-location/internal/server/monitors.go
new file mode 100644
index 00000000..7496b370
--- /dev/null
+++ b/apps/private-location/internal/server/monitors.go
@@ -0,0 +1,159 @@
+package server
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "strconv"
+
+ "connectrpc.com/connect"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/database"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/models"
+ private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+ "github.com/rs/zerolog/log"
+)
+
+// Converts models.NumberComparator to proto NumberComparator
+func convertNumberComparator(m models.NumberComparator) private_locationv1.NumberComparator {
+ switch m {
+ case models.NumberNotEquals:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_NOT_EQUAL
+ case models.NumberEquals:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_EQUAL
+ case models.NumberGreaterThan:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN
+ case models.NumberGreaterThanEqual:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL
+ case models.NumberLowerThan:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN
+ case models.NumberLowerThanEqual:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL
+ default:
+ return private_locationv1.NumberComparator_NUMBER_COMPARATOR_UNSPECIFIED
+ }
+}
+
+// Converts models.StringComparator to proto StringComparator
+func convertStringComparator(m models.StringComparator) private_locationv1.StringComparator {
+ switch m {
+ case models.StringNotEquals:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_NOT_EQUAL
+ case models.StringEquals:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_EQUAL
+ case models.StringContains:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_CONTAINS
+ case models.StringNotContains:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_NOT_CONTAINS
+ case models.StringEmpty:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_EMPTY
+ case models.StringNotEmpty:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_NOT_EMPTY
+ default:
+ return private_locationv1.StringComparator_STRING_COMPARATOR_UNSPECIFIED
+ }
+}
+
+// Helper to parse assertions
+func ParseAssertions(assertions sql.NullString) (
+ statusAssertions []*private_locationv1.StatusCodeAssertion,
+ headerAssertions []*private_locationv1.HeaderAssertion,
+ bodyAssertions []*private_locationv1.BodyAssertion,
+) {
+ if !assertions.Valid {
+ return
+ }
+ var rawAssertions []json.RawMessage
+ if err := json.Unmarshal([]byte(assertions.String), &rawAssertions); err != nil {
+ log.Printf("Failed to unmarshal assertions: %v", err)
+ return
+ }
+ for _, a := range rawAssertions {
+ var assert models.Assertion
+ if err := json.Unmarshal(a, &assert); err != nil {
+ log.Printf("Failed to unmarshal assertion: %v", err)
+ continue
+ }
+ switch assert.AssertionType {
+ case models.AssertionStatus:
+ var target models.StatusTarget
+ if err := json.Unmarshal(a, &target); err != nil {
+ log.Printf("Failed to unmarshal status target: %v", err)
+ continue
+ }
+ statusAssertions = append(statusAssertions, &private_locationv1.StatusCodeAssertion{
+ Target: target.Target,
+ Comparator: convertNumberComparator(target.Comparator),
+ })
+ case models.AssertionHeader:
+ var target models.HeaderTarget
+ if err := json.Unmarshal(a, &target); err != nil {
+ log.Error().Err(err).Msg("unable to encode payload")
+ continue
+ }
+ headerAssertions = append(headerAssertions, &private_locationv1.HeaderAssertion{
+ Key: target.Key,
+ Target: target.Target,
+ Comparator: convertStringComparator(target.Comparator),
+ })
+ case models.AssertionTextBody:
+ var target models.BodyString
+ if err := json.Unmarshal(a, &target); err != nil {
+ log.Printf("Failed to unmarshal body target: %v", err)
+ continue
+ }
+ bodyAssertions = append(bodyAssertions, &private_locationv1.BodyAssertion{
+ Target: target.Target,
+ Comparator: convertStringComparator(target.Comparator),
+ })
+ }
+ }
+ return
+}
+
+func (h *privateLocationHandler) Monitors(ctx context.Context, req *connect.Request[private_locationv1.MonitorsRequest]) (*connect.Response[private_locationv1.MonitorsResponse], error) {
+ token := req.Header().Get("openstatus-token")
+ if token == "" {
+ return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing token"))
+ }
+
+ var monitors []database.Monitor
+ err := h.db.Select(&monitors, "SELECT monitor.* FROM monitor JOIN private_location_to_monitor a ON monitor.id = a.monitor_id JOIN private_location b ON a.private_location_id = b.id WHERE b.token = ? AND monitor.deleted_at IS NULL", token)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+
+ var httpMonitors []*private_locationv1.HTTPMonitor
+ for _, monitor := range monitors {
+ if monitor.JobType != "http" {
+ continue
+ }
+
+ var headers []*private_locationv1.Headers
+ if err := json.Unmarshal([]byte(monitor.Headers), &headers); err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("unable to unmarshal headers")
+ headers = nil
+ }
+
+ statusAssertions, headerAssertions, bodyAssertions := ParseAssertions(monitor.Assertions)
+
+ httpMonitors = append(httpMonitors, &private_locationv1.HTTPMonitor{
+ Url: monitor.URL,
+ Periodicity: monitor.Periodicity,
+ Id: strconv.Itoa(monitor.ID),
+ Method: monitor.Method,
+ Body: monitor.Body,
+ Timeout: monitor.Timeout,
+ DegradedAt: &monitor.DegradedAfter.Int64,
+ FollowRedirects: monitor.FollowRedirects,
+ Headers: headers,
+ StatusCodeAssertions: statusAssertions,
+ HeaderAssertions: headerAssertions,
+ BodyAssertions: bodyAssertions,
+ })
+ }
+
+ return connect.NewResponse(&private_locationv1.MonitorsResponse{
+ HttpMonitors: httpMonitors,
+ }), nil
+}
diff --git a/apps/private-location/internal/server/monitors_test.go b/apps/private-location/internal/server/monitors_test.go
new file mode 100644
index 00000000..5a4c45de
--- /dev/null
+++ b/apps/private-location/internal/server/monitors_test.go
@@ -0,0 +1,57 @@
+package server_test
+
+import (
+ "database/sql"
+ "testing"
+
+ "github.com/openstatushq/openstatus/apps/private-location/internal/server"
+ private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+)
+
+func TestParseAssertions_TextBodyContains(t *testing.T) {
+ // Input JSON for the test
+ input := `[{"version":"v1","type":"textBody","compare":"contains","target":"mydata"}]`
+ assertions := sql.NullString{
+ String: input,
+ Valid: true,
+ }
+
+ _, _, bodyAssertions := server.ParseAssertions(assertions)
+
+ if len(bodyAssertions) != 1 {
+ t.Fatalf("expected 1 body assertion, got %d", len(bodyAssertions))
+ }
+
+ got := bodyAssertions[0]
+ if got.Target != "mydata" {
+ t.Errorf("expected Target to be 'mydata', got '%s'", got.Target)
+ }
+
+ if got.Comparator != private_locationv1.StringComparator_STRING_COMPARATOR_CONTAINS {
+ t.Errorf("expected Comparator to be STRING_COMPARATOR_CONTAINS, got %v", got.Comparator)
+ }
+}
+
+func TestParseAssertions_HttpStatusEquals(t *testing.T) {
+ // Input JSON for the test
+ input := `[{"version":"v1","type":"status","compare":"eq","target":200}]`
+ assertions := sql.NullString{
+ String: input,
+ Valid: true,
+ }
+
+ statusAssertion, _, _ := server.ParseAssertions(assertions)
+
+ if len(statusAssertion) != 1 {
+ t.Fatalf("expected 1 body assertion, got %d", len(statusAssertion))
+ }
+
+ got := statusAssertion[0]
+ if got.Target != 200 {
+ t.Errorf("expected Target to be 'mydata', got '%d'", got.Target)
+ }
+
+ if got.Comparator != private_locationv1.NumberComparator_NUMBER_COMPARATOR_EQUAL {
+ t.Errorf("expected Comparator to be STRING_COMPARATOR_CONTAINS, got %v", got.Comparator)
+ }
+}
diff --git a/apps/private-location/internal/server/routes.go b/apps/private-location/internal/server/routes.go
new file mode 100644
index 00000000..c8a3763a
--- /dev/null
+++ b/apps/private-location/internal/server/routes.go
@@ -0,0 +1,60 @@
+package server
+
+import (
+ "net/http"
+ "os"
+ "time"
+
+ _ "github.com/joho/godotenv/autoload"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/render"
+ "github.com/jmoiron/sqlx"
+ "github.com/openstatushq/openstatus/apps/private-location/internal/tinybird"
+ v1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
+)
+
+// Monitor represents a monitoring job.
+
+type privateLocationHandler struct {
+ db *sqlx.DB
+ TbClient tinybird.Client
+}
+
+func NewPrivateLocationServer(db *sqlx.DB, tbClient tinybird.Client) *privateLocationHandler {
+ return &privateLocationHandler{
+ db: db,
+ TbClient: tbClient,
+ }
+}
+
+// RegisterRoutes sets up the HTTP routes for the server.
+func (s *Server) RegisterRoutes() http.Handler {
+ r := chi.NewRouter()
+ r.Get("/health", s.healthHandler)
+
+ tinyBirdToken := os.Getenv("TINYBIRD_TOKEN")
+
+ httpClient := &http.Client{
+ Timeout: 45 * time.Second,
+ }
+
+ tinybirdClient := tinybird.NewClient(httpClient, tinyBirdToken)
+
+ privateLocationServer := NewPrivateLocationServer(s.db, tinybirdClient)
+ path, handler := v1.NewPrivateLocationServiceHandler(privateLocationServer)
+
+ r.Group(func(r chi.Router) {
+ r.Mount(path, handler)
+ })
+ return r
+}
+
+// healthHandler responds with the health status of the server.
+func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
+
+ render.JSON(w, r, map[string]any{
+ "status": "ok",
+ })
+ render.Status(r, http.StatusOK)
+}
diff --git a/apps/private-location/internal/server/server.go b/apps/private-location/internal/server/server.go
new file mode 100644
index 00000000..48387121
--- /dev/null
+++ b/apps/private-location/internal/server/server.go
@@ -0,0 +1,41 @@
+package server
+
+import (
+ // "database/sql"
+ "fmt"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/joho/godotenv/autoload"
+
+ "github.com/openstatushq/openstatus/apps/private-location/internal/database"
+)
+
+type Server struct {
+ port int
+
+ db *sqlx.DB
+}
+
+func NewServer() *http.Server {
+ port, _ := strconv.Atoi(os.Getenv("PORT"))
+ NewServer := &Server{
+ port: port,
+
+ db: database.New(),
+ }
+
+ // Declare Server config
+ server := &http.Server{
+ Addr: fmt.Sprintf(":%d", NewServer.port),
+ Handler: NewServer.RegisterRoutes(),
+ IdleTimeout: time.Minute,
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ }
+
+ return server
+}
diff --git a/apps/private-location/internal/tinybird/client.go b/apps/private-location/internal/tinybird/client.go
new file mode 100644
index 00000000..5179fb74
--- /dev/null
+++ b/apps/private-location/internal/tinybird/client.go
@@ -0,0 +1,69 @@
+package tinybird
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/rs/zerolog/log"
+)
+
+const baseURL = "https://api.tinybird.co/v0/events"
+
+type Client interface {
+ SendEvent(ctx context.Context, event any, dataSourceName string) error
+}
+
+type client struct {
+ httpClient *http.Client
+ apiKey string
+}
+
+func NewClient(httpClient *http.Client, apiKey string) Client {
+ return client{
+ httpClient: httpClient,
+ apiKey: apiKey,
+ }
+}
+
+func (c client) SendEvent(ctx context.Context, event any, dataSourceName string) error {
+ requestURL, err := url.Parse(baseURL)
+ if err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("unable to parse url")
+ return fmt.Errorf("unable to parse url: %w", err)
+ }
+
+ q := requestURL.Query()
+ q.Add("name", dataSourceName)
+ requestURL.RawQuery = q.Encode()
+
+ var payload bytes.Buffer
+ if err := json.NewEncoder(&payload).Encode(event); err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("unable to encode payload")
+ return fmt.Errorf("unable to encode payload: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload.Bytes()))
+ if err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("unable to create request")
+ return fmt.Errorf("unable to create request: %w", err)
+ }
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ log.Ctx(ctx).Error().Err(err).Msg("unable to send request")
+ return fmt.Errorf("unable to send request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusAccepted {
+ log.Ctx(ctx).Error().Str("status", resp.Status).Msg("unexpected status code")
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ return nil
+}
diff --git a/apps/private-location/internal/tinybird/client_test.go b/apps/private-location/internal/tinybird/client_test.go
new file mode 100644
index 00000000..bc0670be
--- /dev/null
+++ b/apps/private-location/internal/tinybird/client_test.go
@@ -0,0 +1,76 @@
+package tinybird_test
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/openstatushq/openstatus/apps/checker/pkg/tinybird"
+ "github.com/stretchr/testify/require"
+)
+
+type interceptorHTTPClient struct {
+ f func(req *http.Request) (*http.Response, error)
+}
+
+func (i *interceptorHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) {
+ return i.f(req)
+}
+
+func (i *interceptorHTTPClient) GetHTTPClient() *http.Client {
+ return &http.Client{
+ Transport: i,
+ }
+}
+
+func TestSendEvent(t *testing.T) {
+ t.Parallel()
+
+ ctx := t.Context()
+
+ t.Run("it should return an error if it can not send the event", func(t *testing.T) {
+ interceptor := &interceptorHTTPClient{
+ f: func(req *http.Request) (*http.Response, error) {
+ return nil, fmt.Errorf("unable to send request")
+ },
+ }
+
+ client := tinybird.NewClient(interceptor.GetHTTPClient(), "apiKey")
+
+ err := client.SendEvent(ctx, "event", "test")
+ require.Error(t, err)
+ })
+
+ t.Run("it should return an error if the response status code is not 200", func(t *testing.T) {
+ interceptor := &interceptorHTTPClient{
+ f: func(req *http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: http.StatusInternalServerError,
+ }, nil
+ },
+ }
+
+ client := tinybird.NewClient(interceptor.GetHTTPClient(), "apiKey")
+
+ err := client.SendEvent(ctx, "event", "test")
+ require.Error(t, err)
+ })
+
+ t.Run("it should succeed and return nothing", func(t *testing.T) {
+ var url string
+ interceptor := &interceptorHTTPClient{
+ f: func(req *http.Request) (*http.Response, error) {
+ url = req.URL.String()
+ return &http.Response{
+ StatusCode: http.StatusAccepted,
+ }, nil
+ },
+ }
+
+ client := tinybird.NewClient(interceptor.GetHTTPClient(), "apiKey")
+
+ err := client.SendEvent(ctx, "event", "test")
+ require.NoError(t, err)
+ require.Equal(t, "https://api.tinybird.co/v0/events?name=test", url)
+ })
+}
diff --git a/apps/private-location/justfile b/apps/private-location/justfile
new file mode 100644
index 00000000..727428fc
--- /dev/null
+++ b/apps/private-location/justfile
@@ -0,0 +1,25 @@
+# Simple Makefile for a Go project
+
+# Build the application
+all: build test
+
+build:
+ echo "Building..."
+
+
+ go build -o main cmd/server/main.go
+dev:
+ air
+# Run the application
+run:
+ go run cmd/server/main.go
+
+# Test the application
+test:
+ echo "Testing..."
+ go test ./... -v
+
+# Clean the binary
+clean:
+ echo "Cleaning..."
+ rm -f main
diff --git a/apps/private-location/proto/private_location/v1/assertions.pb.go b/apps/private-location/proto/private_location/v1/assertions.pb.go
new file mode 100644
index 00000000..c33032b4
--- /dev/null
+++ b/apps/private-location/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/private-location/proto/private_location/v1/http_monitor.pb.go b/apps/private-location/proto/private_location/v1/http_monitor.pb.go
new file mode 100644
index 00000000..866cdc4f
--- /dev/null
+++ b/apps/private-location/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/private-location/proto/private_location/v1/private_location.connect.go b/apps/private-location/proto/private_location/v1/private_location.connect.go
new file mode 100644
index 00000000..25b3e700
--- /dev/null
+++ b/apps/private-location/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/private-location/proto/private_location/v1/private_location.pb.go b/apps/private-location/proto/private_location/v1/private_location.pb.go
new file mode 100644
index 00000000..af801e36
--- /dev/null
+++ b/apps/private-location/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/private-location/proto/private_location/v1/tcp_monitor.pb.go b/apps/private-location/proto/private_location/v1/tcp_monitor.pb.go
new file mode 100644
index 00000000..d81c9b7d
--- /dev/null
+++ b/apps/private-location/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/web/src/app/(pages)/(content)/play/checker/_components/informations.tsx b/apps/web/src/app/(pages)/(content)/play/checker/_components/informations.tsx
index 2d4d9cb1..37502b6d 100644
--- a/apps/web/src/app/(pages)/(content)/play/checker/_components/informations.tsx
+++ b/apps/web/src/app/(pages)/(content)/play/checker/_components/informations.tsx
@@ -5,7 +5,7 @@ import { Separator } from "@openstatus/ui";
const TOTAL_REGIONS = Object.keys(regionDict).length;
const TOTAL_PROVIDERS = Object.keys(regionDict).reduce((acc, region) => {
return acc.add(regionDict[region as keyof typeof regionDict].provider);
-}, new Set<"fly" | "koyeb" | "railway">());
+}, new Set<"fly" | "koyeb" | "railway" | "private">());
export function Informations() {
return (
diff --git a/apps/web/src/components/monitor-charts/utils.tsx b/apps/web/src/components/monitor-charts/utils.tsx
index ec8f54e0..35af1091 100644
--- a/apps/web/src/components/monitor-charts/utils.tsx
+++ b/apps/web/src/components/monitor-charts/utils.tsx
@@ -1,4 +1,4 @@
-import { regionDict } from "@openstatus/regions";
+import { getRegionInfo, regionDict } from "@openstatus/regions";
import type { Period, Quantile } from "@/lib/monitor/utils";
import type { ResponseGraph } from "@/lib/tb";
@@ -24,7 +24,7 @@ export function groupDataByTimestamp(
(acc, curr) => {
const { timestamp, region } = curr;
const latency = curr[`${quantile}Latency`];
- const { flag, code, location } = regionDict[region];
+ const { flag, code, location } = getRegionInfo(region);
const fullNameRegion = `${code}`;
regions[fullNameRegion] = { flag, code, location }; // to get the region keys
if (timestamp === currentTimestamp) {
diff --git a/apps/web/src/components/monitor-dashboard/response-details.tsx b/apps/web/src/components/monitor-dashboard/response-details.tsx
index ec2288c8..dd86c5c6 100644
--- a/apps/web/src/components/monitor-dashboard/response-details.tsx
+++ b/apps/web/src/components/monitor-dashboard/response-details.tsx
@@ -10,12 +10,11 @@ import { RegionInfo } from "@/components/ping-response-analysis/region-info";
import { ResponseHeaderTable } from "@/components/ping-response-analysis/response-header-table";
import { ResponseTimingTable } from "@/components/ping-response-analysis/response-timing-table";
import { prepareGetByPeriod } from "@/lib/tb";
-import type { Region } from "@openstatus/db/src/schema/constants";
interface ResponseDetailsProps {
monitorId: string;
url?: string | undefined;
- region?: Region;
+ region?: string;
cronTimestamp?: number | undefined;
type: "http" | "tcp";
}
diff --git a/apps/web/src/components/ping-response-analysis/region-info.tsx b/apps/web/src/components/ping-response-analysis/region-info.tsx
index c4c7c342..a0e0210a 100644
--- a/apps/web/src/components/ping-response-analysis/region-info.tsx
+++ b/apps/web/src/components/ping-response-analysis/region-info.tsx
@@ -1,5 +1,4 @@
import { StatusCodeBadge } from "@/components/monitor/status-code-badge";
-import type { Region } from "@openstatus/db/src/schema/constants";
import { latencyFormatter, regionFormatter, timestampFormatter } from "./utils";
export function RegionInfo({
@@ -7,7 +6,7 @@ export function RegionInfo({
error,
}: {
check: {
- region: Region;
+ region: string;
timestamp: number;
latency: number;
status?: number;
diff --git a/apps/web/src/components/ping-response-analysis/utils.ts b/apps/web/src/components/ping-response-analysis/utils.ts
index 5e8dce17..fa5b5bd3 100644
--- a/apps/web/src/components/ping-response-analysis/utils.ts
+++ b/apps/web/src/components/ping-response-analysis/utils.ts
@@ -6,7 +6,7 @@ import {
monitorRegionSchema,
} from "@openstatus/db/src/schema/constants";
import type { Region } from "@openstatus/db/src/schema/constants";
-import { continentDict, regionDict } from "@openstatus/regions";
+import { continentDict, getRegionInfo, regionDict } from "@openstatus/regions";
export function latencyFormatter(value: number) {
return `${new Intl.NumberFormat("us").format(value).toString()}ms`;
@@ -22,10 +22,10 @@ export function continentFormatter(region: Region) {
}
export function regionFormatter(
- region: Region,
+ region: string,
type: "short" | "long" = "short",
) {
- const { code, flag, location } = regionDict[region];
+ const { code, flag, location } = getRegionInfo(region);
if (type === "short") return `${code} ${flag}`;
return `${location} ${flag}`;
}
diff --git a/packages/api/src/edge.ts b/packages/api/src/edge.ts
index 909b70ef..57e3f913 100644
--- a/packages/api/src/edge.ts
+++ b/packages/api/src/edge.ts
@@ -13,6 +13,7 @@ import { monitorTagRouter } from "./router/monitorTag";
import { notificationRouter } from "./router/notification";
import { pageRouter } from "./router/page";
import { pageSubscriberRouter } from "./router/pageSubscriber";
+import { privateLocationRouter } from "./router/privateLocation";
import { statusPageRouter } from "./router/statusPage";
import { statusReportRouter } from "./router/statusReport";
import { tinybirdRouter } from "./router/tinybird";
@@ -42,4 +43,5 @@ export const edgeRouter = createTRPCRouter({
blob: blobRouter,
feedback: feedbackRouter,
statusPage: statusPageRouter,
+ privateLocation: privateLocationRouter,
});
diff --git a/packages/api/src/router/monitor.ts b/packages/api/src/router/monitor.ts
index b767bd97..dcd77d74 100644
--- a/packages/api/src/router/monitor.ts
+++ b/packages/api/src/router/monitor.ts
@@ -26,12 +26,14 @@ import {
notification,
notificationsToMonitors,
page,
+ privateLocationToMonitors,
selectIncidentSchema,
selectMaintenanceSchema,
selectMonitorSchema,
selectMonitorTagSchema,
selectNotificationSchema,
selectPageSchema,
+ selectPrivateLocationSchema,
selectPublicMonitorSchema,
} from "@openstatus/db/src/schema";
@@ -887,6 +889,9 @@ export const monitorRouter = createTRPCRouter({
with: { maintenance: true },
},
incidents: true,
+ privateLocationToMonitors: {
+ with: { privateLocation: true },
+ },
},
});
@@ -899,6 +904,7 @@ export const monitorRouter = createTRPCRouter({
tags: z.array(selectMonitorTagSchema).default([]),
maintenances: z.array(selectMaintenanceSchema).default([]),
incidents: z.array(selectIncidentSchema).default([]),
+ privateLocations: z.array(selectPrivateLocationSchema).default([]),
})
.parse({
...data,
@@ -909,6 +915,9 @@ export const monitorRouter = createTRPCRouter({
tags: data.monitorTagsToMonitors.map((t) => t.monitorTag),
maintenances: data.maintenancesToMonitors.map((m) => m.maintenance),
incidents: data.incidents,
+ privateLocations: data.privateLocationToMonitors.map(
+ (p) => p.privateLocation,
+ ),
});
}),
@@ -1060,6 +1069,7 @@ export const monitorRouter = createTRPCRouter({
id: z.number(),
regions: z.array(z.string()),
periodicity: z.enum(monitorPeriodicity),
+ privateLocations: z.array(z.number()),
}),
)
.mutation(async ({ ctx, input }) => {
@@ -1085,8 +1095,6 @@ export const monitorRouter = createTRPCRouter({
});
}
- console.log(input.regions, limits.regions);
-
if (
input.regions.length > 0 &&
!input.regions.every((r) =>
@@ -1099,15 +1107,30 @@ export const monitorRouter = createTRPCRouter({
});
}
- await ctx.db
- .update(monitor)
- .set({
- regions: input.regions.join(","),
- periodicity: input.periodicity,
- updatedAt: new Date(),
- })
- .where(and(...whereConditions))
- .run();
+ await ctx.db.transaction(async (tx) => {
+ await tx
+ .update(monitor)
+ .set({
+ regions: input.regions.join(","),
+ periodicity: input.periodicity,
+ updatedAt: new Date(),
+ })
+ .where(and(...whereConditions))
+ .run();
+
+ await tx
+ .delete(privateLocationToMonitors)
+ .where(eq(privateLocationToMonitors.monitorId, input.id));
+
+ if (input.privateLocations && input.privateLocations.length > 0) {
+ await tx.insert(privateLocationToMonitors).values(
+ input.privateLocations.map((privateLocationId) => ({
+ monitorId: input.id,
+ privateLocationId,
+ })),
+ );
+ }
+ });
}),
updateResponseTime: protectedProcedure
diff --git a/packages/api/src/router/privateLocation.ts b/packages/api/src/router/privateLocation.ts
new file mode 100644
index 00000000..42c07832
--- /dev/null
+++ b/packages/api/src/router/privateLocation.ts
@@ -0,0 +1,120 @@
+import { and, eq } from "@openstatus/db";
+import {
+ privateLocation,
+ privateLocationToMonitors,
+} from "@openstatus/db/src/schema";
+import { z } from "zod";
+
+import { createTRPCRouter, protectedProcedure } from "../trpc";
+
+export const privateLocationRouter = createTRPCRouter({
+ list: protectedProcedure.query(async (opts) => {
+ const privateLocations = await opts.ctx.db.transaction(async (tx) => {
+ return await tx.query.privateLocation.findMany({
+ where: eq(privateLocation.workspaceId, opts.ctx.workspace.id),
+ with: {
+ privateLocationToMonitors: {
+ with: { monitor: true },
+ },
+ },
+ });
+ });
+ const result = privateLocations.map((privateLocation) => ({
+ ...privateLocation,
+ monitors: privateLocation.privateLocationToMonitors
+ .map((m) => m.monitor)
+ .filter((m) => m !== null),
+ }));
+ return result;
+ }),
+ new: protectedProcedure
+ .input(
+ z.object({
+ name: z.string(),
+ monitors: z.array(z.number()),
+ }),
+ )
+ .mutation(async (opts) => {
+ const token = crypto.randomUUID();
+
+ return await opts.ctx.db.transaction(async (tx) => {
+ const _privateLocation = await tx
+ .insert(privateLocation)
+ .values({
+ name: opts.input.name,
+ token,
+ workspaceId: opts.ctx.workspace.id,
+ })
+ .returning()
+ .get();
+
+ if (opts.input.monitors.length) {
+ await tx.insert(privateLocationToMonitors).values(
+ opts.input.monitors.map((monitorId) => ({
+ privateLocationId: _privateLocation.id,
+ monitorId,
+ })),
+ );
+ }
+ return _privateLocation;
+ });
+ }),
+ update: protectedProcedure
+ .input(
+ z.object({
+ id: z.number(),
+ name: z.string(),
+ monitors: z.array(z.number()),
+ }),
+ )
+ .mutation(async (opts) => {
+ return await opts.ctx.db.transaction(async (tx) => {
+ const _privateLocation = await tx
+ .update(privateLocation)
+ .set({ name: opts.input.name, updatedAt: new Date() })
+ .where(
+ and(
+ eq(privateLocation.id, opts.input.id),
+ eq(privateLocation.workspaceId, opts.ctx.workspace.id),
+ ),
+ )
+ .returning()
+ .get();
+
+ await tx
+ .delete(privateLocationToMonitors)
+ .where(
+ eq(
+ privateLocationToMonitors.privateLocationId,
+ _privateLocation.id,
+ ),
+ );
+
+ if (opts.input.monitors.length) {
+ await tx.insert(privateLocationToMonitors).values(
+ opts.input.monitors.map((monitorId) => ({
+ privateLocationId: _privateLocation.id,
+ monitorId,
+ })),
+ );
+ }
+
+ return _privateLocation;
+ });
+ }),
+ delete: protectedProcedure
+ .input(z.object({ id: z.number() }))
+ .mutation(async (opts) => {
+ console.log("delete private location", opts.input.id);
+ return await opts.ctx.db.transaction(async (tx) => {
+ await tx
+ .delete(privateLocation)
+ .where(
+ and(
+ eq(privateLocation.id, opts.input.id),
+ eq(privateLocation.workspaceId, opts.ctx.workspace.id),
+ ),
+ );
+ });
+ }),
+});
diff --git a/packages/api/src/router/tinybird/index.ts b/packages/api/src/router/tinybird/index.ts
index fdb7e070..d5f9db4d 100644
--- a/packages/api/src/router/tinybird/index.ts
+++ b/packages/api/src/router/tinybird/index.ts
@@ -168,7 +168,7 @@ export const tinybirdRouter = createTRPCRouter({
.input(
z.object({
monitorId: z.string(),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
}),
)
@@ -181,7 +181,7 @@ export const tinybirdRouter = createTRPCRouter({
.input(
z.object({
monitorId: z.string(),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
from: z.coerce.date().optional(),
to: z.coerce.date().optional(),
@@ -224,7 +224,7 @@ export const tinybirdRouter = createTRPCRouter({
fromDate: z.string().optional(),
toDate: z.string().optional(),
interval: z.number().int().optional(), // in minutes, default 30
- regions: z.enum(monitorRegions).array().optional(),
+ regions: z.enum(monitorRegions).or(z.string()).array().optional(),
type: z.enum(types).default("http"),
period: z.enum(["7d", "30d"]).default("30d"),
}),
@@ -286,7 +286,7 @@ export const tinybirdRouter = createTRPCRouter({
monitorId: z.string(),
period: z.enum(periods),
type: z.enum(types).default("http"),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
cronTimestamp: z.number().int().optional(),
}),
)
@@ -321,7 +321,7 @@ export const tinybirdRouter = createTRPCRouter({
monitorId: z.string(),
period: z.enum(periods),
type: z.enum(types).default("http"),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
}),
)
@@ -355,7 +355,7 @@ export const tinybirdRouter = createTRPCRouter({
monitorId: z.string(),
period: z.enum(periods),
type: z.enum(types).default("http"),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
}),
)
@@ -391,7 +391,7 @@ export const tinybirdRouter = createTRPCRouter({
type: z.enum(types).default("http"),
// Additional filters
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
cronTimestamp: z.number().int().optional(),
}),
)
@@ -429,7 +429,7 @@ export const tinybirdRouter = createTRPCRouter({
monitorIds: z.string().array(),
period: z.enum(["45d"]),
type: z.enum(types).default("http"),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
}),
)
@@ -519,7 +519,7 @@ export const tinybirdRouter = createTRPCRouter({
z.object({
monitorId: z.string(),
period: z.enum(periods),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
type: z.enum(types).default("http"),
}),
)
@@ -541,7 +541,7 @@ export const tinybirdRouter = createTRPCRouter({
monitorId: z.string(),
period: z.enum(periods),
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
type: z.literal("http"),
}),
)
diff --git a/packages/db/drizzle/0048_neat_tempest.sql b/packages/db/drizzle/0048_neat_tempest.sql
new file mode 100644
index 00000000..ae72d297
--- /dev/null
+++ b/packages/db/drizzle/0048_neat_tempest.sql
@@ -0,0 +1,19 @@
+CREATE TABLE `private_location` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `token` text NOT NULL,
+ `last_seen_at` integer,
+ `workspace_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `updated_at` integer DEFAULT (strftime('%s', 'now')),
+ FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `private_location_to_monitor` (
+ `private_location_id` integer,
+ `monitor_id` integer,
+ `created_at` integer DEFAULT (strftime('%s', 'now')),
+ `deleted_at` integer,
+ FOREIGN KEY (`private_location_id`) REFERENCES `private_location`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade
+);
diff --git a/packages/db/drizzle/meta/0048_snapshot.json b/packages/db/drizzle/meta/0048_snapshot.json
new file mode 100644
index 00000000..0b32b7fd
--- /dev/null
+++ b/packages/db/drizzle/meta/0048_snapshot.json
@@ -0,0 +1,2587 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "091d5fb3-ae4f-4ce2-a91f-695d054c0f79",
+ "prevId": "686060ea-046b-414a-909f-1b087e086065",
+ "tables": {
+ "workspace": {
+ "name": "workspace",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripe_id": {
+ "name": "stripe_id",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ends_at": {
+ "name": "ends_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "paid_until": {
+ "name": "paid_until",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "limits": {
+ "name": "limits",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "dsn": {
+ "name": "dsn",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "workspace_slug_unique": {
+ "name": "workspace_slug_unique",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ },
+ "workspace_stripe_id_unique": {
+ "name": "workspace_stripe_id_unique",
+ "columns": [
+ "stripe_id"
+ ],
+ "isUnique": true
+ },
+ "workspace_id_dsn_unique": {
+ "name": "workspace_id_dsn_unique",
+ "columns": [
+ "id",
+ "dsn"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "account": {
+ "name": "account",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_provider_account_id_pk": {
+ "columns": [
+ "provider",
+ "provider_account_id"
+ ],
+ "name": "account_provider_provider_account_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "photo_url": {
+ "name": "photo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "user_tenant_id_unique": {
+ "name": "user_tenant_id_unique",
+ "columns": [
+ "tenant_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_to_workspaces": {
+ "name": "users_to_workspaces",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "users_to_workspaces_user_id_user_id_fk": {
+ "name": "users_to_workspaces_user_id_user_id_fk",
+ "tableFrom": "users_to_workspaces",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "users_to_workspaces_workspace_id_workspace_id_fk": {
+ "name": "users_to_workspaces_workspace_id_workspace_id_fk",
+ "tableFrom": "users_to_workspaces",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "users_to_workspaces_user_id_workspace_id_pk": {
+ "columns": [
+ "user_id",
+ "workspace_id"
+ ],
+ "name": "users_to_workspaces_user_id_workspace_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verification_token": {
+ "name": "verification_token",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verification_token_identifier_token_pk": {
+ "columns": [
+ "identifier",
+ "token"
+ ],
+ "name": "verification_token_identifier_token_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "status_report_to_monitors": {
+ "name": "status_report_to_monitors",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status_report_id": {
+ "name": "status_report_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "status_report_to_monitors_monitor_id_monitor_id_fk": {
+ "name": "status_report_to_monitors_monitor_id_monitor_id_fk",
+ "tableFrom": "status_report_to_monitors",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "status_report_to_monitors_status_report_id_status_report_id_fk": {
+ "name": "status_report_to_monitors_status_report_id_status_report_id_fk",
+ "tableFrom": "status_report_to_monitors",
+ "tableTo": "status_report",
+ "columnsFrom": [
+ "status_report_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "status_report_to_monitors_monitor_id_status_report_id_pk": {
+ "columns": [
+ "monitor_id",
+ "status_report_id"
+ ],
+ "name": "status_report_to_monitors_monitor_id_status_report_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "status_report": {
+ "name": "status_report",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "status_report_workspace_id_workspace_id_fk": {
+ "name": "status_report_workspace_id_workspace_id_fk",
+ "tableFrom": "status_report",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "status_report_page_id_page_id_fk": {
+ "name": "status_report_page_id_page_id_fk",
+ "tableFrom": "status_report",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "status_report_update": {
+ "name": "status_report_update",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "date": {
+ "name": "date",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status_report_id": {
+ "name": "status_report_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "status_report_update_status_report_id_status_report_id_fk": {
+ "name": "status_report_update_status_report_id_status_report_id_fk",
+ "tableFrom": "status_report_update",
+ "tableTo": "status_report",
+ "columnsFrom": [
+ "status_report_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integration": {
+ "name": "integration",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "credential": {
+ "name": "credential",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integration_workspace_id_workspace_id_fk": {
+ "name": "integration_workspace_id_workspace_id_fk",
+ "tableFrom": "integration",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "page": {
+ "name": "page",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "custom_domain": {
+ "name": "custom_domain",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "published": {
+ "name": "published",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "force_theme": {
+ "name": "force_theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'system'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_protected": {
+ "name": "password_protected",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "homepage_url": {
+ "name": "homepage_url",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "contact_url": {
+ "name": "contact_url",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "legacy_page": {
+ "name": "legacy_page",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "configuration": {
+ "name": "configuration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "show_monitor_values": {
+ "name": "show_monitor_values",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "page_slug_unique": {
+ "name": "page_slug_unique",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "page_workspace_id_workspace_id_fk": {
+ "name": "page_workspace_id_workspace_id_fk",
+ "tableFrom": "page",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor": {
+ "name": "monitor",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "job_type": {
+ "name": "job_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'http'"
+ },
+ "periodicity": {
+ "name": "periodicity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'other'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "active": {
+ "name": "active",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "regions": {
+ "name": "regions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "url": {
+ "name": "url",
+ "type": "text(2048)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "headers": {
+ "name": "headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'GET'"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "timeout": {
+ "name": "timeout",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 45000
+ },
+ "degraded_after": {
+ "name": "degraded_after",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "assertions": {
+ "name": "assertions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "otel_endpoint": {
+ "name": "otel_endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "otel_headers": {
+ "name": "otel_headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "public": {
+ "name": "public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "retry": {
+ "name": "retry",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 3
+ },
+ "follow_redirects": {
+ "name": "follow_redirects",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "monitor_workspace_id_workspace_id_fk": {
+ "name": "monitor_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitors_to_pages": {
+ "name": "monitors_to_pages",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "monitors_to_pages_monitor_id_monitor_id_fk": {
+ "name": "monitors_to_pages_monitor_id_monitor_id_fk",
+ "tableFrom": "monitors_to_pages",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "monitors_to_pages_page_id_page_id_fk": {
+ "name": "monitors_to_pages_page_id_page_id_fk",
+ "tableFrom": "monitors_to_pages",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "monitors_to_pages_monitor_id_page_id_pk": {
+ "columns": [
+ "monitor_id",
+ "page_id"
+ ],
+ "name": "monitors_to_pages_monitor_id_page_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "page_subscriber": {
+ "name": "page_subscriber",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "page_subscriber_page_id_page_id_fk": {
+ "name": "page_subscriber_page_id_page_id_fk",
+ "tableFrom": "page_subscriber",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification": {
+ "name": "notification",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{}'"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notification_workspace_id_workspace_id_fk": {
+ "name": "notification_workspace_id_workspace_id_fk",
+ "tableFrom": "notification",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_trigger": {
+ "name": "notification_trigger",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cron_timestamp": {
+ "name": "cron_timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "notification_id_monitor_id_crontimestampe": {
+ "name": "notification_id_monitor_id_crontimestampe",
+ "columns": [
+ "notification_id",
+ "monitor_id",
+ "cron_timestamp"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "notification_trigger_monitor_id_monitor_id_fk": {
+ "name": "notification_trigger_monitor_id_monitor_id_fk",
+ "tableFrom": "notification_trigger",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_trigger_notification_id_notification_id_fk": {
+ "name": "notification_trigger_notification_id_notification_id_fk",
+ "tableFrom": "notification_trigger",
+ "tableTo": "notification",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notifications_to_monitors": {
+ "name": "notifications_to_monitors",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notifications_to_monitors_monitor_id_monitor_id_fk": {
+ "name": "notifications_to_monitors_monitor_id_monitor_id_fk",
+ "tableFrom": "notifications_to_monitors",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_to_monitors_notification_id_notification_id_fk": {
+ "name": "notifications_to_monitors_notification_id_notification_id_fk",
+ "tableFrom": "notifications_to_monitors",
+ "tableTo": "notification",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "notifications_to_monitors_monitor_id_notification_id_pk": {
+ "columns": [
+ "monitor_id",
+ "notification_id"
+ ],
+ "name": "notifications_to_monitors_monitor_id_notification_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_status": {
+ "name": "monitor_status",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "region": {
+ "name": "region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "monitor_status_idx": {
+ "name": "monitor_status_idx",
+ "columns": [
+ "monitor_id",
+ "region"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "monitor_status_monitor_id_monitor_id_fk": {
+ "name": "monitor_status_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_status",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "monitor_status_monitor_id_region_pk": {
+ "columns": [
+ "monitor_id",
+ "region"
+ ],
+ "name": "monitor_status_monitor_id_region_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "invitation": {
+ "name": "invitation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "incident": {
+ "name": "incident",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'triage'"
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "acknowledged_at": {
+ "name": "acknowledged_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "acknowledged_by": {
+ "name": "acknowledged_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_at": {
+ "name": "resolved_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resolved_by": {
+ "name": "resolved_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "incident_screenshot_url": {
+ "name": "incident_screenshot_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "recovery_screenshot_url": {
+ "name": "recovery_screenshot_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "auto_resolved": {
+ "name": "auto_resolved",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "incident_monitor_id_started_at_unique": {
+ "name": "incident_monitor_id_started_at_unique",
+ "columns": [
+ "monitor_id",
+ "started_at"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "incident_monitor_id_monitor_id_fk": {
+ "name": "incident_monitor_id_monitor_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set default",
+ "onUpdate": "no action"
+ },
+ "incident_workspace_id_workspace_id_fk": {
+ "name": "incident_workspace_id_workspace_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_acknowledged_by_user_id_fk": {
+ "name": "incident_acknowledged_by_user_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "acknowledged_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "incident_resolved_by_user_id_fk": {
+ "name": "incident_resolved_by_user_id_fk",
+ "tableFrom": "incident",
+ "tableTo": "user",
+ "columnsFrom": [
+ "resolved_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_tag": {
+ "name": "monitor_tag",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "monitor_tag_workspace_id_workspace_id_fk": {
+ "name": "monitor_tag_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor_tag",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_tag_to_monitor": {
+ "name": "monitor_tag_to_monitor",
+ "columns": {
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_tag_id": {
+ "name": "monitor_tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "monitor_tag_to_monitor_monitor_id_monitor_id_fk": {
+ "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_tag_to_monitor",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": {
+ "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk",
+ "tableFrom": "monitor_tag_to_monitor",
+ "tableTo": "monitor_tag",
+ "columnsFrom": [
+ "monitor_tag_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": {
+ "columns": [
+ "monitor_id",
+ "monitor_tag_id"
+ ],
+ "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "application": {
+ "name": "application",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dsn": {
+ "name": "dsn",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {
+ "application_dsn_unique": {
+ "name": "application_dsn_unique",
+ "columns": [
+ "dsn"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "application_workspace_id_workspace_id_fk": {
+ "name": "application_workspace_id_workspace_id_fk",
+ "tableFrom": "application",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "maintenance": {
+ "name": "maintenance",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from": {
+ "name": "from",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "to": {
+ "name": "to",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "maintenance_workspace_id_workspace_id_fk": {
+ "name": "maintenance_workspace_id_workspace_id_fk",
+ "tableFrom": "maintenance",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "maintenance_page_id_page_id_fk": {
+ "name": "maintenance_page_id_page_id_fk",
+ "tableFrom": "maintenance",
+ "tableTo": "page",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "maintenance_to_monitor": {
+ "name": "maintenance_to_monitor",
+ "columns": {
+ "maintenance_id": {
+ "name": "maintenance_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "maintenance_to_monitor_maintenance_id_maintenance_id_fk": {
+ "name": "maintenance_to_monitor_maintenance_id_maintenance_id_fk",
+ "tableFrom": "maintenance_to_monitor",
+ "tableTo": "maintenance",
+ "columnsFrom": [
+ "maintenance_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "maintenance_to_monitor_monitor_id_monitor_id_fk": {
+ "name": "maintenance_to_monitor_monitor_id_monitor_id_fk",
+ "tableFrom": "maintenance_to_monitor",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "maintenance_to_monitor_maintenance_id_monitor_id_pk": {
+ "columns": [
+ "maintenance_id",
+ "monitor_id"
+ ],
+ "name": "maintenance_to_monitor_maintenance_id_monitor_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "check": {
+ "name": "check",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "regions": {
+ "name": "regions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "url": {
+ "name": "url",
+ "type": "text(4096)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'GET'"
+ },
+ "count_requests": {
+ "name": "count_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 1
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "check_workspace_id_workspace_id_fk": {
+ "name": "check_workspace_id_workspace_id_fk",
+ "tableFrom": "check",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "monitor_run": {
+ "name": "monitor_run",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "runned_at": {
+ "name": "runned_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "monitor_run_workspace_id_workspace_id_fk": {
+ "name": "monitor_run_workspace_id_workspace_id_fk",
+ "tableFrom": "monitor_run",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "monitor_run_monitor_id_monitor_id_fk": {
+ "name": "monitor_run_monitor_id_monitor_id_fk",
+ "tableFrom": "monitor_run",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "private_location": {
+ "name": "private_location",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "private_location_workspace_id_workspace_id_fk": {
+ "name": "private_location_workspace_id_workspace_id_fk",
+ "tableFrom": "private_location",
+ "tableTo": "workspace",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "private_location_to_monitor": {
+ "name": "private_location_to_monitor",
+ "columns": {
+ "private_location_id": {
+ "name": "private_location_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "monitor_id": {
+ "name": "monitor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(strftime('%s', 'now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "private_location_to_monitor_private_location_id_private_location_id_fk": {
+ "name": "private_location_to_monitor_private_location_id_private_location_id_fk",
+ "tableFrom": "private_location_to_monitor",
+ "tableTo": "private_location",
+ "columnsFrom": [
+ "private_location_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "private_location_to_monitor_monitor_id_monitor_id_fk": {
+ "name": "private_location_to_monitor_monitor_id_monitor_id_fk",
+ "tableFrom": "private_location_to_monitor",
+ "tableTo": "monitor",
+ "columnsFrom": [
+ "monitor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
index de765e57..ce009ffb 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -337,6 +337,13 @@
"when": 1757840904190,
"tag": "0047_nifty_roughhouse",
"breakpoints": true
+ },
+ {
+ "idx": 48,
+ "version": "6",
+ "when": 1760602903085,
+ "tag": "0048_neat_tempest",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts
index 5b1c0e28..da395b0c 100644
--- a/packages/db/src/schema/index.ts
+++ b/packages/db/src/schema/index.ts
@@ -15,3 +15,4 @@ export * from "./applications";
export * from "./maintenances";
export * from "./check";
export * from "./monitor_run";
+export * from "./private_locations";
diff --git a/packages/db/src/schema/monitors/monitor.ts b/packages/db/src/schema/monitors/monitor.ts
index 48917906..107cf0ab 100644
--- a/packages/db/src/schema/monitors/monitor.ts
+++ b/packages/db/src/schema/monitors/monitor.ts
@@ -13,6 +13,7 @@ import { monitorStatusTable } from "../monitor_status/monitor_status";
import { monitorTagsToMonitors } from "../monitor_tags";
import { notificationsToMonitors } from "../notifications";
import { page } from "../pages";
+import { privateLocationToMonitors } from "../private_locations";
import { monitorsToStatusReport } from "../status_reports";
import { workspace } from "../workspaces/workspace";
import { monitorJobTypes, monitorMethods, monitorStatus } from "./constants";
@@ -82,6 +83,7 @@ export const monitorRelation = relations(monitor, ({ one, many }) => ({
maintenancesToMonitors: many(maintenancesToMonitors),
incidents: many(incidentTable),
monitorStatus: many(monitorStatusTable),
+ privateLocationToMonitors: many(privateLocationToMonitors),
}));
export const monitorsToPages = sqliteTable(
diff --git a/packages/db/src/schema/plan/config.ts b/packages/db/src/schema/plan/config.ts
index 2eb2833f..07f1d093 100644
--- a/packages/db/src/schema/plan/config.ts
+++ b/packages/db/src/schema/plan/config.ts
@@ -1,4 +1,4 @@
-import { type Region, availableRegions } from "../constants";
+import { AVAILABLE_REGIONS, FREE_FLY_REGIONS } from "@openstatus/regions";
import type { WorkspacePlan } from "../workspaces/validation";
import type { Limits } from "./schema";
@@ -51,7 +51,7 @@ export const allPlans: Record<
"notification-channels": 1,
members: 1,
"audit-log": false,
- regions: ["ams", "gru", "iad", "jnb", "hkg", "syd"] satisfies Region[],
+ regions: [...FREE_FLY_REGIONS],
"private-locations": false,
},
},
@@ -89,7 +89,7 @@ export const allPlans: Record<
"notification-channels": 10,
members: "Unlimited",
"audit-log": false,
- regions: [...availableRegions],
+ regions: [...AVAILABLE_REGIONS],
"private-locations": false,
},
},
@@ -107,7 +107,7 @@ export const allPlans: Record<
"synthetic-checks": 300,
periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"],
"multi-region": true,
- "max-regions": availableRegions.length,
+ "max-regions": AVAILABLE_REGIONS.length,
"data-retention": "12 months",
"status-pages": 5,
maintenance: true,
@@ -127,7 +127,7 @@ export const allPlans: Record<
"notification-channels": 20,
members: "Unlimited",
"audit-log": true,
- regions: [...availableRegions],
+ regions: [...AVAILABLE_REGIONS],
"private-locations": false,
},
},
diff --git a/packages/db/src/schema/plan/schema.ts b/packages/db/src/schema/plan/schema.ts
index 558a88eb..d40e58f0 100644
--- a/packages/db/src/schema/plan/schema.ts
+++ b/packages/db/src/schema/plan/schema.ts
@@ -1,3 +1,4 @@
+import { FREE_FLY_REGIONS } from "@openstatus/regions";
import { z } from "zod";
import { monitorPeriodicitySchema, monitorRegionSchema } from "../constants";
@@ -17,9 +18,7 @@ export const limitsSchema = z.object({
"data-retention": z
.enum(["14 days", "3 months", "12 months", "24 months"])
.default("14 days"),
- regions: monitorRegionSchema
- .array()
- .default(["ams", "gru", "iad", "jnb", "sin", "syd"]),
+ regions: monitorRegionSchema.array().default(FREE_FLY_REGIONS),
"private-locations": z.boolean().default(false),
screenshots: z.boolean().default(false),
"response-logs": z.boolean().default(false),
diff --git a/packages/db/src/schema/plan/utils.ts b/packages/db/src/schema/plan/utils.ts
index 85dcb115..b749a83d 100644
--- a/packages/db/src/schema/plan/utils.ts
+++ b/packages/db/src/schema/plan/utils.ts
@@ -42,3 +42,57 @@ export function getPriceConfig(plan: WorkspacePlan, currency?: string) {
}
return { value: planConfig.price.USD, locale: "en-US", currency: "USD" };
}
+
+export function getPlansForLimit(
+ currentPlan: WorkspacePlan,
+ limit: keyof Limits,
+): WorkspacePlan[] {
+ const currentLimitValue = allPlans[currentPlan].limits[limit];
+ const planOrder: WorkspacePlan[] = ["free", "starter", "team"];
+
+ // Get plans that come after the current plan
+ const availablePlans = planOrder.filter((plan) => {
+ const planIndex = planOrder.indexOf(plan);
+ const currentIndex = planOrder.indexOf(currentPlan);
+ return planIndex > currentIndex;
+ });
+
+ // Filter plans based on the limit feature value
+ return availablePlans.filter((plan) => {
+ const planLimitValue = allPlans[plan].limits[limit];
+
+ // For boolean limits, only show plans where the feature is enabled
+ if (typeof currentLimitValue === "boolean") {
+ return planLimitValue === true;
+ }
+
+ // For numeric limits, show plans with higher values
+ if (
+ typeof currentLimitValue === "number" &&
+ typeof planLimitValue === "number"
+ ) {
+ return planLimitValue > currentLimitValue;
+ }
+
+ // For array limits (e.g., periodicity, regions), show plans with more options
+ if (Array.isArray(currentLimitValue) && Array.isArray(planLimitValue)) {
+ return planLimitValue.length > currentLimitValue.length;
+ }
+
+ // For string limits (e.g., data-retention), check if it's "better"
+ // This is a simple heuristic - could be improved based on specific needs
+ if (
+ typeof currentLimitValue === "string" &&
+ typeof planLimitValue === "string"
+ ) {
+ return planLimitValue !== currentLimitValue;
+ }
+
+ // For "Unlimited" string literal in members
+ if (planLimitValue === "Unlimited") {
+ return true;
+ }
+
+ return false;
+ });
+}
diff --git a/packages/db/src/schema/private_locations/index.ts b/packages/db/src/schema/private_locations/index.ts
new file mode 100644
index 00000000..d3b132c7
--- /dev/null
+++ b/packages/db/src/schema/private_locations/index.ts
@@ -0,0 +1,2 @@
+export * from "./private_locations";
+export * from "./validation";
diff --git a/packages/db/src/schema/private_locations/private_locations.ts b/packages/db/src/schema/private_locations/private_locations.ts
new file mode 100644
index 00000000..ae01c0be
--- /dev/null
+++ b/packages/db/src/schema/private_locations/private_locations.ts
@@ -0,0 +1,61 @@
+import { relations } from "drizzle-orm";
+import { sql } from "drizzle-orm/sql";
+import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
+import { monitor } from "../monitors/monitor";
+import { workspace } from "../workspaces";
+
+export const privateLocation = sqliteTable("private_location", {
+ id: integer("id").primaryKey(),
+ name: text("name").notNull(),
+ token: text("token").notNull(),
+ lastSeenAt: integer("last_seen_at", { mode: "timestamp" }),
+ workspaceId: integer("workspace_id").references(() => workspace.id),
+ createdAt: integer("created_at", { mode: "timestamp" }).default(
+ sql`(strftime('%s', 'now'))`,
+ ),
+ updatedAt: integer("updated_at", { mode: "timestamp" }).default(
+ sql`(strftime('%s', 'now'))`,
+ ),
+});
+
+export const privateLocationToMonitors = sqliteTable(
+ "private_location_to_monitor",
+ {
+ privateLocationId: integer("private_location_id").references(
+ () => privateLocation.id,
+ { onDelete: "cascade" },
+ ),
+ monitorId: integer("monitor_id").references(() => monitor.id, {
+ onDelete: "cascade",
+ }),
+ createdAt: integer("created_at", { mode: "timestamp" }).default(
+ sql`(strftime('%s', 'now'))`,
+ ),
+ deletedAt: integer("deleted_at", { mode: "timestamp" }),
+ },
+);
+
+export const privateLocationRelation = relations(
+ privateLocation,
+ ({ many, one }) => ({
+ privateLocationToMonitors: many(privateLocationToMonitors),
+ workspace: one(workspace, {
+ fields: [privateLocation.workspaceId],
+ references: [workspace.id],
+ }),
+ }),
+);
+
+export const privateLocationToMonitorsRelation = relations(
+ privateLocationToMonitors,
+ ({ one }) => ({
+ privateLocation: one(privateLocation, {
+ fields: [privateLocationToMonitors.privateLocationId],
+ references: [privateLocation.id],
+ }),
+ monitor: one(monitor, {
+ fields: [privateLocationToMonitors.monitorId],
+ references: [monitor.id],
+ }),
+ }),
+);
diff --git a/packages/db/src/schema/private_locations/validation.ts b/packages/db/src/schema/private_locations/validation.ts
new file mode 100644
index 00000000..2cf3805c
--- /dev/null
+++ b/packages/db/src/schema/private_locations/validation.ts
@@ -0,0 +1,11 @@
+import { createInsertSchema, createSelectSchema } from "drizzle-zod";
+import type { z } from "zod";
+
+import { privateLocation } from "./private_locations";
+
+export const insertPrivateLocationSchema = createInsertSchema(privateLocation);
+
+export const selectPrivateLocationSchema = createSelectSchema(privateLocation);
+
+export type InsertPrivateLocation = z.infer;
+export type PrivateLocation = z.infer;
diff --git a/packages/db/src/seed.mts b/packages/db/src/seed.mts
index f0e4f400..9219d7a0 100644
--- a/packages/db/src/seed.mts
+++ b/packages/db/src/seed.mts
@@ -13,6 +13,8 @@ import {
notification,
notificationsToMonitors,
page,
+ privateLocation,
+ privateLocationToMonitors,
statusReport,
statusReportUpdate,
user,
@@ -50,6 +52,16 @@ async function main() {
endsAt: null,
paidUntil: null,
},
+ {
+ id: 3,
+ slug: "test3",
+ stripeId: "stripeId3",
+ name: "test3",
+ subscriptionId: "subscriptionId3",
+ plan: "team",
+ endsAt: null,
+ paidUntil: null,
+ },
])
.onConflictDoNothing()
.run();
@@ -105,6 +117,16 @@ async function main() {
otelEndpoint: "https://otel.com:4337",
otelHeaders: '[{"key":"Authorization","value":"Basic"}]',
},
+ {
+ id: 5,
+ active: true,
+ workspaceId: 3,
+ periodicity: "10m",
+ url: "https://openstat.us",
+ method: "GET",
+ regions: "ams",
+ public: true,
+ },
])
.onConflictDoNothing()
.run();
@@ -291,6 +313,27 @@ async function main() {
})
.onConflictDoNothing()
.run();
+
+ await db
+ .insert(privateLocation)
+ .values({
+ id: 1,
+ name: "My Home",
+ token: "my-secret-key",
+ workspaceId: 3,
+ createdAt: new Date(),
+ })
+ .onConflictDoNothing()
+ .run();
+ await db
+ .insert(privateLocationToMonitors)
+ .values({
+ privateLocationId: 1,
+ monitorId: 5,
+ createdAt: new Date(),
+ })
+ .onConflictDoNothing()
+ .run();
process.exit(0);
}
diff --git a/packages/proto/buf.gen.yaml b/packages/proto/buf.gen.yaml
new file mode 100644
index 00000000..9c632232
--- /dev/null
+++ b/packages/proto/buf.gen.yaml
@@ -0,0 +1,29 @@
+# buf.gen.yaml defines a local generation template.
+# For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml
+version: v2
+# managed:
+# enabled: true
+# override:
+# - file_option: go_package_prefix
+# value: github.com/openstatushq/openstatus/packages/proto/gen
+#
+# This is not beautiful but while in dev let's generate it twice
+plugins:
+ - local: protoc-gen-go
+ out: ../../apps/private-location/proto
+ opt:
+ - paths=source_relative
+ - local: protoc-gen-connect-go
+ out: ../../apps/private-location/proto
+ opt:
+ - paths=source_relative
+ - package_suffix
+ - local: protoc-gen-go
+ out: ../../apps/checker/proto
+ opt:
+ - paths=source_relative
+ - local: protoc-gen-connect-go
+ out: ../../apps/checker/proto
+ opt:
+ - paths=source_relative
+ - package_suffix
diff --git a/packages/proto/buf.yaml b/packages/proto/buf.yaml
new file mode 100644
index 00000000..95bfe736
--- /dev/null
+++ b/packages/proto/buf.yaml
@@ -0,0 +1,8 @@
+version: v2
+
+modules:
+ - path: .
+ name: buf.build/openstatus/private-location
+lint:
+ use:
+ - STANDARD
diff --git a/packages/proto/go.mod b/packages/proto/go.mod
new file mode 100644
index 00000000..388fb2ea
--- /dev/null
+++ b/packages/proto/go.mod
@@ -0,0 +1,8 @@
+module github.com/openstatushq/openstatus/packages/proto
+
+go 1.25.2
+
+require (
+ connectrpc.com/connect v1.19.1
+ google.golang.org/protobuf v1.36.10
+)
diff --git a/packages/proto/go.sum b/packages/proto/go.sum
new file mode 100644
index 00000000..5e575aa6
--- /dev/null
+++ b/packages/proto/go.sum
@@ -0,0 +1,6 @@
+connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
+connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
diff --git a/packages/proto/justfile b/packages/proto/justfile
new file mode 100644
index 00000000..b89b6441
--- /dev/null
+++ b/packages/proto/justfile
@@ -0,0 +1,13 @@
+set fallback := true
+
+init:
+ go install github.com/bufbuild/buf/cmd/buf@latest
+ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
+ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
+ go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
+
+buf:
+ pnpm buf
+
+gen:
+ buf generate
diff --git a/packages/proto/package.json b/packages/proto/package.json
new file mode 100644
index 00000000..d8c3454e
--- /dev/null
+++ b/packages/proto/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@openstatus/proto",
+ "version": "0.0.0",
+ "scripts": {
+ "buf": "buf generate"
+ },
+ "devDependencies": {
+ "@bufbuild/buf": "1.57.2"
+ }
+}
diff --git a/packages/proto/private_location/v1/assertions.proto b/packages/proto/private_location/v1/assertions.proto
new file mode 100644
index 00000000..0f494b66
--- /dev/null
+++ b/packages/proto/private_location/v1/assertions.proto
@@ -0,0 +1,47 @@
+
+syntax = "proto3";
+
+package private_location.v1;
+
+option go_package = "github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1";
+
+
+enum NumberComparator {
+ 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;
+}
+
+enum StringComparator {
+ 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;
+}
+
+message StatusCodeAssertion {
+ int64 target = 1;
+ NumberComparator comparator = 2;
+}
+
+message BodyAssertion {
+ string target = 1;
+ StringComparator comparator = 2;
+}
+
+message HeaderAssertion {
+ string target = 1;
+ StringComparator comparator = 2;
+ string key = 3;
+}
diff --git a/packages/proto/private_location/v1/http_monitor.proto b/packages/proto/private_location/v1/http_monitor.proto
new file mode 100644
index 00000000..444ddb8a
--- /dev/null
+++ b/packages/proto/private_location/v1/http_monitor.proto
@@ -0,0 +1,32 @@
+syntax = "proto3";
+
+package private_location.v1;
+
+import "private_location/v1/assertions.proto";
+
+option go_package = "github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1";
+
+
+message Headers {
+ string key = 1;
+ string value = 2;
+}
+
+message HTTPMonitor {
+ string id = 1;
+ string url = 2;
+ string periodicity = 3;
+ string method = 4;
+ string body = 5;
+ int64 timeout = 6;
+ optional int64 degraded_at = 7;
+ int64 retry = 8;
+ bool follow_redirects = 9;
+
+ repeated Headers headers = 10;
+
+ repeated StatusCodeAssertion status_code_assertions = 11;
+ repeated BodyAssertion body_assertions = 12;
+ repeated HeaderAssertion header_assertions = 13;
+
+}
diff --git a/packages/proto/private_location/v1/private_location.proto b/packages/proto/private_location/v1/private_location.proto
new file mode 100644
index 00000000..8d420852
--- /dev/null
+++ b/packages/proto/private_location/v1/private_location.proto
@@ -0,0 +1,61 @@
+syntax = "proto3";
+
+package private_location.v1;
+
+import "private_location/v1/http_monitor.proto";
+import "private_location/v1/tcp_monitor.proto";
+
+
+option go_package = "github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1";
+
+service PrivateLocationService {
+ rpc Monitors(MonitorsRequest) returns (MonitorsResponse) {}
+ rpc IngestTCP(IngestTCPRequest) returns (IngestTCPResponse) {}
+ rpc IngestHTTP(IngestHTTPRequest) returns (IngestHTTPResponse) {}
+}
+
+message MonitorsRequest {}
+
+message MonitorsResponse {
+ repeated HTTPMonitor http_monitors = 1;
+ repeated TCPMonitor tcp_monitors = 2;
+}
+
+
+message IngestTCPRequest {
+ string id = 1;
+ string monitorId = 2;
+ int64 latency = 3;
+ int64 timestamp = 4;
+ int64 cronTimestamp = 5;
+ string uri = 6;
+ string message = 7;
+ string requestStatus = 8;
+ int64 error = 9;
+ string timing = 10;
+
+}
+
+message IngestTCPResponse {
+
+}
+
+message IngestHTTPRequest {
+ string id = 1;
+ string monitorId = 2;
+ int64 latency = 3;
+ int64 timestamp = 4;
+ int64 cronTimestamp = 5;
+ string url = 6;
+ string requestStatus = 7;
+ string message = 8;
+ string body = 9;
+ string headers = 10;
+ string timing = 11;
+ int64 statusCode = 12;
+ int64 error = 13;
+}
+
+message IngestHTTPResponse {
+
+}
diff --git a/packages/proto/private_location/v1/tcp_monitor.proto b/packages/proto/private_location/v1/tcp_monitor.proto
new file mode 100644
index 00000000..b14cfef9
--- /dev/null
+++ b/packages/proto/private_location/v1/tcp_monitor.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package private_location.v1;
+
+option go_package = "github.com/openstatushq/openstatus/packages/proto/private_location/v1;v1";
+
+
+
+
+message TCPMonitor {
+ string id = 1;
+ string uri = 2;
+ int64 timeout = 3;
+ optional int64 degraded_at = 4;
+ string periodicity = 5;
+ int64 retry = 6;
+
+}
diff --git a/packages/regions/index.ts b/packages/regions/index.ts
index a2bbc722..93cd4ced 100644
--- a/packages/regions/index.ts
+++ b/packages/regions/index.ts
@@ -83,7 +83,7 @@ export type RegionInfo = {
flag: string;
continent: Continent;
deprecated: boolean;
- provider: "fly" | "koyeb" | "railway";
+ provider: "fly" | "koyeb" | "railway" | "private";
};
// TODO: we could think of doing the inverse and use "EU" as key
@@ -464,8 +464,8 @@ export const AVAILABLE_REGIONS = ALL_REGIONS.filter(
(r) => !regionDict[r].deprecated,
);
-export function formatRegionCode(region: RegionInfo | Region) {
- const r = typeof region === "string" ? regionDict[region] : region;
+export function formatRegionCode(region: RegionInfo | Region | string) {
+ const r = typeof region === "string" ? getRegionInfo(region) : region;
const suffix = r.code.replace(/(koyeb_|railway_|fly_)/g, "");
if (r.provider === "railway") {
@@ -475,6 +475,25 @@ export function formatRegionCode(region: RegionInfo | Region) {
return suffix;
}
+// NOTE: opts is used to override the location for private locations
+export function getRegionInfo(region: string, opts?: { location?: string }) {
+ if (region in regionDict) {
+ return regionDict[region as Region];
+ }
+
+ return {
+ code: region,
+ location: opts?.location ?? "Private Location",
+ flag: "🌐",
+ continent: "Private",
+ deprecated: false,
+ provider: "private",
+ } satisfies Omit & {
+ code: string;
+ continent: "Private";
+ };
+}
+
export const groupByContinent = Object.entries(regionDict).reduce<
Record
>(
diff --git a/packages/tinybird/src/client.ts b/packages/tinybird/src/client.ts
index 99c78437..cbec9800 100644
--- a/packages/tinybird/src/client.ts
+++ b/packages/tinybird/src/client.ts
@@ -51,7 +51,7 @@ export class OSTinybird {
statusCode: z.number().int().nullable(),
monitorId: z.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -76,7 +76,7 @@ export class OSTinybird {
statusCode: z.number().int().nullable(),
monitorId: z.string(),
requestStatus: z.enum(["error", "success", "degraded"]).nullable(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -98,7 +98,7 @@ export class OSTinybird {
statusCode: z.number().int().nullable(),
monitorId: z.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -123,7 +123,7 @@ export class OSTinybird {
statusCode: z.number().int().nullable(),
monitorId: z.string(),
requestStatus: z.enum(["error", "success", "degraded"]).nullable(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -145,7 +145,7 @@ export class OSTinybird {
statusCode: z.number().int().nullable(),
monitorId: z.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -170,7 +170,7 @@ export class OSTinybird {
statusCode: z.number().int().nullable(),
monitorId: z.string(),
requestStatus: z.enum(["error", "success", "degraded"]).nullable(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -184,7 +184,7 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__http_metrics_1d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
@@ -207,7 +207,7 @@ export class OSTinybird {
pipe: "endpoint__http_metrics_1d__v1",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -230,7 +230,7 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__http_metrics_7d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
@@ -253,7 +253,7 @@ export class OSTinybird {
pipe: "endpoint__http_metrics_7d__v1",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -276,7 +276,7 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__http_metrics_14d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
@@ -299,7 +299,7 @@ export class OSTinybird {
pipe: "endpoint__http_metrics_14d__v1",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -326,7 +326,7 @@ export class OSTinybird {
monitorId: z.string(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -346,7 +346,7 @@ export class OSTinybird {
monitorId: z.string(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -366,7 +366,7 @@ export class OSTinybird {
monitorId: z.string(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -383,10 +383,10 @@ export class OSTinybird {
pipe: "endpoint__http_metrics_by_region_1d__v0",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
count: z.number().int(),
ok: z.number().int(),
p50Latency: z.number().nullable().default(0),
@@ -404,10 +404,10 @@ export class OSTinybird {
pipe: "endpoint__http_metrics_by_region_7d__v0",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
count: z.number().int(),
ok: z.number().int(),
p50Latency: z.number().nullable().default(0),
@@ -425,10 +425,10 @@ export class OSTinybird {
pipe: "endpoint__http_metrics_by_region_14d__v0",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
count: z.number().int(),
ok: z.number().int(),
p50Latency: z.number().nullable().default(0),
@@ -518,7 +518,7 @@ export class OSTinybird {
monitorId: z.string(),
url: z.string().url(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
message: z.string().nullable(),
headers: headersSchema,
@@ -540,7 +540,7 @@ export class OSTinybird {
pipe: "endpoint__http_get_30d__v0",
parameters: z.object({
monitorId: z.string(),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
}),
data: z.object({
@@ -550,7 +550,7 @@ export class OSTinybird {
monitorId: z.string(),
url: z.string().url(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
message: z.string().nullable(),
headers: headersSchema,
@@ -583,7 +583,7 @@ export class OSTinybird {
.number()
.default(0)
.transform((val) => val !== 0),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int().optional(),
message: z.string().nullable().optional(),
timing: timingSchema,
@@ -605,7 +605,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -629,7 +629,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
requestStatus: z.enum(["error", "success", "degraded"]).nullable(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -649,7 +649,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -673,7 +673,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
requestStatus: z.enum(["error", "success", "degraded"]).nullable(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -693,7 +693,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -717,7 +717,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
requestStatus: z.enum(["error", "success", "degraded"]).nullable(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -730,7 +730,7 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__tcp_metrics_1d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
@@ -753,7 +753,7 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_1d__v1",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -777,7 +777,7 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_7d__v0",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -799,7 +799,7 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_7d__v1",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -822,7 +822,7 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__tcp_metrics_14d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
@@ -845,7 +845,7 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_14d__v1",
parameters: z.object({
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
monitorId: z.string(),
}),
data: z.object({
@@ -868,12 +868,12 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__tcp_metrics_by_interval_1d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -889,12 +889,12 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__tcp_metrics_by_interval_7d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -910,12 +910,12 @@ export class OSTinybird {
return this.tb.buildPipe({
pipe: "endpoint__tcp_metrics_by_interval_14d__v0",
parameters: z.object({
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
interval: z.number().int().optional(),
monitorId: z.string(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -932,10 +932,10 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_by_region_1d__v0",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
count: z.number().int(),
ok: z.number().int(),
p50Latency: z.number().nullable().default(0),
@@ -953,10 +953,10 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_by_region_7d__v0",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
count: z.number().int(),
ok: z.number().int(),
p50Latency: z.number().nullable().default(0),
@@ -974,10 +974,10 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_by_region_14d__v0",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
count: z.number().int(),
ok: z.number().int(),
p50Latency: z.number().nullable().default(0),
@@ -1099,7 +1099,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.coerce.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -1116,7 +1116,7 @@ export class OSTinybird {
pipe: "endpoint__tcp_get_30d__v0",
parameters: z.object({
monitorId: z.string(),
- region: z.enum(monitorRegions).optional(),
+ region: z.enum(monitorRegions).or(z.string()).optional(),
cronTimestamp: z.number().int().optional(),
}),
data: z.object({
@@ -1124,7 +1124,7 @@ export class OSTinybird {
latency: z.number().int(),
monitorId: z.string(),
error: z.coerce.boolean(),
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
cronTimestamp: z.number().int(),
trigger: z.enum(triggers).nullable().default("cron"),
timestamp: z.number(),
@@ -1149,7 +1149,7 @@ export class OSTinybird {
regions: z.string().array().optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -1170,7 +1170,7 @@ export class OSTinybird {
regions: z.string().array().optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -1191,7 +1191,7 @@ export class OSTinybird {
regions: z.string().array().optional(),
}),
data: z.object({
- region: z.enum(monitorRegions),
+ region: z.enum(monitorRegions).or(z.string()),
timestamp: z.number().int(),
p50Latency: z.number().nullable().default(0),
p75Latency: z.number().nullable().default(0),
@@ -1210,7 +1210,7 @@ export class OSTinybird {
monitorId: z.string(),
fromDate: z.string().optional(),
toDate: z.string().optional(),
- regions: z.enum(monitorRegions).array().optional(),
+ regions: z.enum(monitorRegions).or(z.string()).array().optional(),
interval: z.number().int().optional(),
}),
data: z.object({
@@ -1229,7 +1229,7 @@ export class OSTinybird {
monitorId: z.string(),
fromDate: z.string().optional(),
toDate: z.string().optional(),
- regions: z.enum(monitorRegions).array().optional(),
+ regions: z.enum(monitorRegions).or(z.string()).array().optional(),
interval: z.number().int().optional(),
}),
data: z.object({
@@ -1248,7 +1248,7 @@ export class OSTinybird {
monitorId: z.string(),
fromDate: z.string().optional(),
toDate: z.string().optional(),
- regions: z.enum(monitorRegions).array().optional(),
+ regions: z.enum(monitorRegions).or(z.string()).array().optional(),
interval: z.number().int().optional(),
}),
data: z.object({
@@ -1267,7 +1267,7 @@ export class OSTinybird {
monitorId: z.string(),
fromDate: z.string().optional(),
toDate: z.string().optional(),
- regions: z.enum(monitorRegions).array().optional(),
+ regions: z.enum(monitorRegions).or(z.string()).array().optional(),
interval: z.number().int().optional(),
}),
data: z.object({
@@ -1353,7 +1353,7 @@ export class OSTinybird {
parameters: z.object({
monitorId: z.string(),
interval: z.number().int().optional(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
timestamp: z.number().int(),
@@ -1444,7 +1444,7 @@ export class OSTinybird {
pipe: "endpoint__tcp_metrics_latency_1d__v1",
parameters: z.object({
monitorId: z.string(),
- regions: z.array(z.enum(monitorRegions)).optional(),
+ regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
}),
data: z.object({
timestamp: z.number().int(),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6eb469f9..d4906d8d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1621,6 +1621,12 @@ importers:
specifier: 5.7.2
version: 5.7.2
+ packages/proto:
+ devDependencies:
+ '@bufbuild/buf':
+ specifier: 1.57.2
+ version: 1.57.2
+
packages/react:
dependencies:
react:
@@ -2506,6 +2512,53 @@ packages:
cpu: [x64]
os: [win32]
+ '@bufbuild/buf-darwin-arm64@1.57.2':
+ resolution: {integrity: sha512-ybwjZ8hFpDiSfTY8ltuNulh6e4CYcojZX+joo/VocFb4InRFyF08S+JSO+1ld+nTmJ24ziGwXZTg8qXiiN6qog==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@bufbuild/buf-darwin-x64@1.57.2':
+ resolution: {integrity: sha512-uoNjkkyjmJlzQyfPI/mwlutDvu48gFg8pYzlK0RhNshvTWCDo2vXbw1gZSaQnv1xK585N9Axmyjp0K2sYJkaDQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@bufbuild/buf-linux-aarch64@1.57.2':
+ resolution: {integrity: sha512-JvB9M+GraP7tgYlyKUhF46+pw/hctFyAjcLvPCPxr8Lr9uo3I6uqU8KywE4AuzdFReaZ4wtdF6sUlsfDOo8Geg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@bufbuild/buf-linux-armv7@1.57.2':
+ resolution: {integrity: sha512-13ZBU/LTboW7B/qjMV6fY6z8q0uyfSpp+y2J+/PAzCrC64ewSTE30pMKFQujMQ05Po0kY5e9yWvTElJcLbzt5w==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@bufbuild/buf-linux-x64@1.57.2':
+ resolution: {integrity: sha512-R28Y6g0kGlM/agtVc8sAjzGhgtbA4JyJYg4w7ZPm/FaVEw2sAYZBsa5uxmMNNOmWopKfd0yr2Pgq8ndfG0QSug==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@bufbuild/buf-win32-arm64@1.57.2':
+ resolution: {integrity: sha512-6YlPr2Z0VLae/76raYDEcotN7YGNgn3qaNy7x5idAoNeAr0EK2WsKtlBypK0/fQO3GOrYo0HGM/0SFENXoQKbw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@bufbuild/buf-win32-x64@1.57.2':
+ resolution: {integrity: sha512-7YKz2RVPQlL+rJYzNL7JK3LpvZUz7wY6hof3QOTNX3sHY6pRD+mtWt/PITTlC3PtyjSKy17wAhbkokC0JErsHw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@bufbuild/buf@1.57.2':
+ resolution: {integrity: sha512-N51MOnjROdtBX3fPU2KGdmwfiT1WWXzs8Jw1tMMkyVAmCSahWbObGIPq7e1fw7lR5i0s3bTxQyk3KRmb2/uwNg==}
+ engines: {node: '>=12'}
+ hasBin: true
+
'@bundled-es-modules/cookie@2.0.1':
resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==}
@@ -13117,6 +13170,37 @@ snapshots:
'@biomejs/cli-win32-x64@1.9.4':
optional: true
+ '@bufbuild/buf-darwin-arm64@1.57.2':
+ optional: true
+
+ '@bufbuild/buf-darwin-x64@1.57.2':
+ optional: true
+
+ '@bufbuild/buf-linux-aarch64@1.57.2':
+ optional: true
+
+ '@bufbuild/buf-linux-armv7@1.57.2':
+ optional: true
+
+ '@bufbuild/buf-linux-x64@1.57.2':
+ optional: true
+
+ '@bufbuild/buf-win32-arm64@1.57.2':
+ optional: true
+
+ '@bufbuild/buf-win32-x64@1.57.2':
+ optional: true
+
+ '@bufbuild/buf@1.57.2':
+ optionalDependencies:
+ '@bufbuild/buf-darwin-arm64': 1.57.2
+ '@bufbuild/buf-darwin-x64': 1.57.2
+ '@bufbuild/buf-linux-aarch64': 1.57.2
+ '@bufbuild/buf-linux-armv7': 1.57.2
+ '@bufbuild/buf-linux-x64': 1.57.2
+ '@bufbuild/buf-win32-arm64': 1.57.2
+ '@bufbuild/buf-win32-x64': 1.57.2
+
'@bundled-es-modules/cookie@2.0.1':
dependencies:
cookie: 0.7.2