Skip to content

Commit c50ee29

Browse files
authored
Merge pull request #2 from openstatusHQ/feat/data-table-endpoints
feat: add data-table pipe endpoints
2 parents 6719fd7 + f733104 commit c50ee29

9 files changed

Lines changed: 210 additions & 47 deletions

File tree

README.md

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Currently supported [notification channels](#notification-channels) are:
1919
- Campsite
2020
- Telegram
2121

22+
We also provide a 6 min. [YouTube](https://www.youtube.com/watch?v=cpurWC9Co1U) video for more visual content. If you have any trouble, feel free to open an issue, join our [Discord](https://openstatus.dev/discord) or message us to [ping@openstatus.dev](mailto:ping@openstatus.dev)
23+
24+
If storing the data to [Tinybird](https://www.tinybird.co), you can use the [OpenStatus Light Viewer](https://data-table.openstatus.dev/light) to filter and learn from your pings.
25+
2226
## Getting Started
2327

2428
To work/test it locally:
@@ -56,28 +60,59 @@ Add `--dry-run` to the commands if you want to first test them:
5660
```bash
5761
tb push tb/datasources/http_ping_responses.datasource
5862
tb push tb/pipes/endpoint__get_http.pipe
63+
tb push tb/pipes/endpoint__get_http_stats.pipe
64+
tb push tb/pipes/endpoint__get_http_facets.pipe
5965
```
6066

6167
#### API endpoint
6268

63-
If you've connected your database to Tinybird, we provide you a simple `GET` endpoint to query your periodical pings.
69+
If you've connected your database to Tinybird, we provide you a some simple `GET` endpoint to query your pings.
70+
71+
The `/api/get` endpoint (`endpoint__get_http.pipe`) accepts the following query parameters:
72+
73+
| **Parameter** | **Type** | **Description** |
74+
| ---------------- | -------- | ----------------------------------------------------- |
75+
| `pageIndex` | `number` | The page number for pagination (starts at 0) |
76+
| `pageSize` | `number` | Number of results per page (defaults to 100) |
77+
| `orderBy` | `string` | Column to sort by (defaults to 'timestamp') |
78+
| `orderDir` | `string` | Sort direction - 'ASC' or 'DESC' (defaults to 'DESC') |
79+
| `latencyStart` | `number` | Filter results with latency >= this value (ms) |
80+
| `latencyEnd` | `number` | Filter results with latency <= this value (ms) |
81+
| `statuses` | `string` | Filter by HTTP status codes (comma-separated list) |
82+
| `regions` | `string` | Filter by edge regions (comma-separated list) |
83+
| `methods` | `string` | Filter by HTTP methods (comma-separated list) |
84+
| `url` | `string` | Filter URLs containing this string |
85+
| `timestampStart` | `number` | Unix timestamp in ms (defaults to 30 days prior) |
86+
| `timestampEnd` | `number` | Unix timestamp in ms (defaults to now) |
87+
88+
Moreover, to make best use of the [OpenStatus Light Viewer](https://data-table.openstatus.dev/light), we support two more API endpoints. They both extend the above query params.
89+
90+
- `/api/stats` endpoint (`endpoint__get_http_stats.pipe`) for the chart values (incl. `interval` param)
6491

65-
The endpoint accepts the following query parameters:
92+
| **Parameter** | **Type** | **Description** |
93+
| ------------- | -------- | ----------------------------------------------------------------------------------- |
94+
| `interval` | `number` | Interval of grouped data in minutes (defaults to 1_440 minutes equivalent to 1 day) |
6695

67-
| **Parameter** | **Type** | **Description** |
68-
| -------------- | -------- | ----------------------------------------------------- |
69-
| `pageIndex` | `number` | The page number for pagination (starts at 0) |
70-
| `pageSize` | `number` | Number of results per page (defaults to 100) |
71-
| `orderBy` | `string` | Column to sort by (defaults to 'timestamp') |
72-
| `orderDir` | `string` | Sort direction - 'ASC' or 'DESC' (defaults to 'DESC') |
73-
| `latencyStart` | `number` | Filter results with latency >= this value (ms) |
74-
| `latencyEnd` | `number` | Filter results with latency <= this value (ms) |
75-
| `statuses` | `string` | Filter by HTTP status codes (comma-separated list) |
76-
| `regions` | `string` | Filter by edge regions (comma-separated list) |
77-
| `methods` | `string` | Filter by HTTP methods (comma-separated list) |
78-
| `url` | `string` | Filter URLs containing this string |
96+
- `/api/facets` endpoint (`endpoint__get_http_facets.pipe`) for the table facets (no additional param)
7997

80-
> Remember that this `/api/get` endpoint will be accessible by anyone if you are not securing it yourself.
98+
All endpoints are just a layer to access the tinybird pipe responses. They share the following response object example:
99+
100+
```json
101+
{
102+
"meta": [],
103+
"data": [],
104+
"rows": 20,
105+
"rows_before_limit_at_least": 100,
106+
"statistics": {
107+
"elapsed": 0.004015279,
108+
"rows_read": 2845,
109+
"bytes_read": 725311
110+
}
111+
}
112+
```
113+
114+
> [!NOTE]
115+
> The `/api/get`, `/api/stats`, `/api/facets` endpoints will be accessible by anyone if you are not securing it yourself.
81116
82117
## Configuration
83118

api/_ping.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,12 @@ async function check(request: PingRequest): Promise<PingResponse> {
8282
clearTimeout(timeoutId);
8383

8484
const latency = end - start;
85-
const body = (await res.text()).slice(0, 1000); // limit to 1000 characters to avoid saving large payloads in tb
85+
const text = await res.text();
8686
const headers = Object.fromEntries(res.headers.entries());
8787

88+
// NOTE: we limit the body to 1000 characters to avoid saving large payloads in tb
89+
const body = text.length > 1000 ? text.slice(0, 1000) + "..." : text;
90+
8891
return {
8992
url: request.url,
9093
method: request.method,

api/facets.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const EVENT_NAME = "endpoint__get_http_facets__v0";
2+
3+
export async function GET(req: Request) {
4+
if (!process.env.TINYBIRD_TOKEN) {
5+
return new Response("No Connected Database", { status: 200 });
6+
}
7+
8+
const reqUrl = new URL(req.url);
9+
const searchParams = reqUrl.searchParams;
10+
11+
const tbUrl = new URL(
12+
`https://api.tinybird.co/v0/pipes/${EVENT_NAME}.json?${searchParams.toString()}`
13+
);
14+
15+
const result = await fetch(tbUrl, {
16+
method: "GET",
17+
headers: {
18+
Authorization: `Bearer ${process.env.TINYBIRD_TOKEN}`,
19+
},
20+
})
21+
.then((r) => r.json())
22+
.then((r) => r)
23+
.catch((e) => e.toString());
24+
25+
if (!result?.data) {
26+
console.error(`Error with: ${JSON.stringify(result)}`);
27+
return new Response("Internal Server Error", { status: 500 });
28+
}
29+
30+
return new Response(JSON.stringify(result), {
31+
headers: { "Content-Type": "application/json" },
32+
});
33+
}

api/get.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,10 @@ export async function GET(req: Request) {
77

88
const reqUrl = new URL(req.url);
99
const searchParams = reqUrl.searchParams;
10-
const pageIndex = searchParams.get("pageIndex");
11-
const pageSize = searchParams.get("pageSize");
12-
const orderBy = searchParams.get("orderBy");
13-
const orderDir = searchParams.get("orderDir");
14-
const latencyStart = searchParams.get("latencyStart");
15-
const latencyEnd = searchParams.get("latencyEnd");
16-
const url = searchParams.get("url");
17-
// REMINDER: can be comma separated list as tb supports arrays.
18-
const statuses = searchParams.get("statuses");
19-
const methods = searchParams.get("methods");
20-
const regions = searchParams.get("regions");
2110

22-
const tbUrl = new URL(`https://api.tinybird.co/v0/pipes/${EVENT_NAME}.json`);
23-
24-
// Only set params if they are provided
25-
if (pageIndex) tbUrl.searchParams.set("pageIndex", pageIndex);
26-
if (pageSize) tbUrl.searchParams.set("pageSize", pageSize);
27-
if (orderBy) tbUrl.searchParams.set("orderBy", orderBy);
28-
if (orderDir) tbUrl.searchParams.set("orderDir", orderDir);
29-
if (latencyStart) tbUrl.searchParams.set("latencyStart", latencyStart);
30-
if (latencyEnd) tbUrl.searchParams.set("latencyEnd", latencyEnd);
31-
if (statuses) tbUrl.searchParams.set("statuses", statuses);
32-
if (methods) tbUrl.searchParams.set("methods", methods);
33-
if (regions) tbUrl.searchParams.set("regions", regions);
34-
if (url) tbUrl.searchParams.set("url", url);
11+
const tbUrl = new URL(
12+
`https://api.tinybird.co/v0/pipes/${EVENT_NAME}.json?${searchParams.toString()}`
13+
);
3514

3615
const result = await fetch(tbUrl, {
3716
method: "GET",
@@ -48,7 +27,7 @@ export async function GET(req: Request) {
4827
return new Response("Internal Server Error", { status: 500 });
4928
}
5029

51-
return new Response(JSON.stringify(result.data), {
30+
return new Response(JSON.stringify(result), {
5231
headers: { "Content-Type": "application/json" },
5332
});
5433
}

api/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const region = process.env.VERCEL_REGION || "unknown";
66

77
const META_DESCRIPTION =
88
"Lightweight one-click solution to monitor your endpoints across multiple regions.";
9-
const META_TITLE = "OpenStatus | Vercel Edge Ping";
9+
const META_TITLE = "OpenStatus Light | Vercel Edge Ping";
1010
const META_URL = "https://light.openstatus.dev";
1111
const META_OG_IMAGE =
1212
"https://www.openstatus.dev/api/og?title=Vercel%20Edge%20Ping&description=Lightweight%20one-click%20solution%20to%20monitor%20your%20endpoints%20across%20multiple%20regions.&footer=light.openstatus.dev";
@@ -63,9 +63,13 @@ export async function GET(): Promise<Response> {
6363
<p>
6464
Be notified via Slack, Discord, Campsite or Telegram if >50% of the regions are down.
6565
</p>
66-
<p>
67-
Get the latest cron requests by visiting <a href="/api/get">/api/get</a>.
68-
</p>
66+
<p>More endpoints (with search params support):</p>
67+
<ul>
68+
<li>Get the latest requests by visiting <a href="/api/get">/api/get</a>.</li>
69+
<li>Get the latest stats by visiting <a href="/api/stats">/api/stats</a>.</li>
70+
<li>Get the latest facets by visiting <a href="/api/facets">/api/facets</a>.</li>
71+
</ul>
72+
<p>Once deployed, access your data by visiting <a href="https://logs.run/light">logs.run/light</a> and changing the base URL to your endpoint.</p>
6973
<p>
7074
Read more on <a href="https://github.com/openstatusHQ/vercel-edge-ping">GitHub</a>
7175
&#183;

api/stats.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const EVENT_NAME = "endpoint__get_http_stats__v0";
2+
3+
export async function GET(req: Request) {
4+
if (!process.env.TINYBIRD_TOKEN) {
5+
return new Response("No Connected Database", { status: 200 });
6+
}
7+
8+
const reqUrl = new URL(req.url);
9+
const searchParams = reqUrl.searchParams;
10+
11+
const tbUrl = new URL(
12+
`https://api.tinybird.co/v0/pipes/${EVENT_NAME}.json?${searchParams.toString()}`
13+
);
14+
15+
const result = await fetch(tbUrl, {
16+
method: "GET",
17+
headers: {
18+
Authorization: `Bearer ${process.env.TINYBIRD_TOKEN}`,
19+
},
20+
})
21+
.then((r) => r.json())
22+
.then((r) => r)
23+
.catch((e) => e.toString());
24+
25+
if (!result?.data) {
26+
console.error(`Error with: ${JSON.stringify(result)}`);
27+
return new Response("Internal Server Error", { status: 500 });
28+
}
29+
30+
return new Response(JSON.stringify(result), {
31+
headers: { "Content-Type": "application/json" },
32+
});
33+
}

tb/pipes/endpoint__get_http.pipe

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@ NODE endpoint
44
SQL >
55

66
%
7-
SELECT *
7+
SELECT
8+
*,
9+
multiIf(
10+
status >= 200 AND status < 400,
11+
'success',
12+
status >= 400 AND status < 500,
13+
'warning',
14+
status >= 500,
15+
'error',
16+
'unknown' -- Default case if none of the above match
17+
) as level
818
from http_ping_responses__v0
919
WHERE
1020
1 = 1
@@ -16,10 +26,22 @@ SQL >
1626
{% if defined(url) %} AND url ILIKE concat('%', {{ String(url) }}, '%') {% end %}
1727
{% if defined(timestampStart) %} AND timestamp >= {{ Int64(timestampStart) }} {% end %}
1828
{% if defined(timestampEnd) %} AND timestamp <= {{ Int64(timestampEnd) }} {% end %}
29+
{% if defined(levels) %}
30+
AND multiIf(
31+
status >= 200 AND status < 400,
32+
'success',
33+
status >= 400 AND status < 500,
34+
'warning',
35+
status >= 500,
36+
'error',
37+
'unknown'
38+
)
39+
IN {{ Array(levels, String) }}
40+
{% end %}
1941
ORDER BY
2042
{{ column(orderBy, 'timestamp') }}
2143
{% if defined(orderDir) and String(orderDir) == "ASC" %} ASC
2244
{% else %} DESC
2345
{% end %}
24-
LIMIT {{ Int32(pageSize, 100) }}
46+
{% if defined(pageSize) %} LIMIT {{ Int32(pageSize, 100) }} {% end %}
2547
OFFSET {{ Int32(pageIndex, 0) * Int32(pageSize, 100) }}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
VERSION 0
2+
3+
NODE endpoint
4+
SQL >
5+
6+
%
7+
SELECT facet, value, COUNT(*) AS count
8+
FROM
9+
(
10+
SELECT
11+
arrayJoin(
12+
[
13+
('method', method),
14+
('url', url),
15+
('status', toString(status)),
16+
('region', region),
17+
('latency', toString(latency)),
18+
('level', level)
19+
]
20+
) AS pair,
21+
pair .1 AS facet,
22+
pair .2 AS value
23+
FROM endpoint__get_http__v0
24+
)
25+
GROUP BY facet, value
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
VERSION 0
2+
3+
NODE endpoint
4+
SQL >
5+
6+
%
7+
WITH
8+
filtered_data AS (
9+
SELECT timestamp, status FROM endpoint
10+
{% if defined(timestampStart) and defined(timestampEnd) %}
11+
UNION ALL
12+
SELECT {{ Int64(timestampStart) }} AS timestamp, 0 AS status -- Start boundary
13+
UNION ALL
14+
SELECT {{ Int64(timestampEnd) }} AS timestamp, 0 AS status -- End boundary
15+
{% end %}
16+
)
17+
SELECT
18+
toUnixTimestamp(
19+
toStartOfInterval(
20+
toDateTime(timestamp / 1000), INTERVAL {{ Int32(interval, 1_440) }} MINUTE
21+
)
22+
) * 1000 AS timestamp,
23+
countIf(status >= 200 AND status < 400) AS success,
24+
countIf(status >= 400 AND status < 500) AS warning,
25+
countIf(status >= 500) AS error
26+
FROM filtered_data
27+
GROUP BY timestamp
28+
ORDER BY timestamp
29+
WITH FILL STEP {{ Int32(interval, 1_440) }} * 60 * 1000

0 commit comments

Comments
 (0)