Skip to content

Commit 31f0e08

Browse files
committed
timestamp in clickhouse
1 parent c6d77ba commit 31f0e08

File tree

7 files changed

+438
-21
lines changed

7 files changed

+438
-21
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ NEXT_PUBLIC_API_URL="http://localhost:3001"
4242

4343
# Not Necessary unless using blog
4444
MARBLE_WORKSPACE_KEY=
45-
MARBLE_API_URL=https://api.marblecms.com
45+
MARBLE_API_URL=https://api.marblecms.com
46+
47+
UPSTASH_QSTASH_TOKEN="i love you upstash"

apps/api/src/query/builders/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ProfilesBuilders } from "./profiles";
1010
import { SessionsBuilders } from "./sessions";
1111
import { SummaryBuilders } from "./summary";
1212
import { TrafficBuilders } from "./traffic";
13+
import { UptimeBuilders } from "./uptime";
1314
import { VitalsBuilders } from "./vitals";
1415

1516
export const QueryBuilders = {
@@ -26,6 +27,7 @@ export const QueryBuilders = {
2627
...LinksBuilders,
2728
...EngagementBuilders,
2829
...VitalsBuilders,
30+
...UptimeBuilders,
2931
};
3032

3133
export type QueryType = keyof typeof QueryBuilders;
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import type { Filter, SimpleQueryConfig } from "../types";
2+
3+
/**
4+
* Uptime monitoring query builders
5+
* Uses uptime.uptime_monitor table
6+
*
7+
* Fields:
8+
* - site_id: Website identifier
9+
* - url: Monitored URL
10+
* - timestamp: Check timestamp
11+
* - status: 1 = up, 0 = down
12+
* - http_code: HTTP response code
13+
* - ttfb_ms: Time to first byte (ms)
14+
* - total_ms: Total response time (ms)
15+
* - ssl_expiry: SSL certificate expiry date
16+
* - ssl_valid: SSL certificate validity (1 = valid, 0 = invalid)
17+
* - probe_region: Region where check was performed
18+
*/
19+
20+
const UPTIME_TABLE = "uptime.uptime_monitor";
21+
22+
export const UptimeBuilders: Record<string, SimpleQueryConfig> = {
23+
uptime_overview: {
24+
customSql: (websiteId: string, startDate: string, endDate: string) => ({
25+
sql: `
26+
SELECT
27+
COUNT(*) as total_checks,
28+
sum(status) as successful_checks,
29+
COUNT(*) - sum(status) as failed_checks,
30+
round((sum(status) / COUNT(*)) * 100, 2) as uptime_percentage,
31+
avg(total_ms) as avg_response_time,
32+
quantile(0.50)(total_ms) as p50_response_time,
33+
quantile(0.75)(total_ms) as p75_response_time,
34+
quantile(0.95)(total_ms) as p95_response_time,
35+
quantile(0.99)(total_ms) as p99_response_time,
36+
max(total_ms) as max_response_time,
37+
min(total_ms) as min_response_time,
38+
avg(ttfb_ms) as avg_ttfb,
39+
any(ssl_expiry) as ssl_expiry,
40+
min(ssl_valid) as ssl_valid
41+
FROM ${UPTIME_TABLE}
42+
WHERE
43+
site_id = {websiteId:String}
44+
AND timestamp >= toDateTime({startDate:String})
45+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
46+
`,
47+
params: { websiteId, startDate, endDate },
48+
}),
49+
timeField: "timestamp",
50+
customizable: false,
51+
},
52+
53+
uptime_time_series: {
54+
customSql: (
55+
websiteId: string,
56+
startDate: string,
57+
endDate: string,
58+
_filters?: Filter[],
59+
_granularity?: string
60+
) => {
61+
const granularity = _granularity ?? "hour";
62+
const timeGroup =
63+
granularity === "minute"
64+
? "toStartOfMinute(timestamp)"
65+
: granularity === "hour"
66+
? "toStartOfHour(timestamp)"
67+
: granularity === "day"
68+
? "toDate(timestamp)"
69+
: "toStartOfHour(timestamp)";
70+
71+
return {
72+
sql: `
73+
SELECT
74+
${timeGroup} as date,
75+
COUNT(*) as total_checks,
76+
sum(status) as successful_checks,
77+
COUNT(*) - sum(status) as failed_checks,
78+
round((sum(status) / COUNT(*)) * 100, 2) as uptime_percentage,
79+
avg(total_ms) as avg_response_time,
80+
quantile(0.50)(total_ms) as p50_response_time,
81+
quantile(0.95)(total_ms) as p95_response_time,
82+
max(total_ms) as max_response_time
83+
FROM ${UPTIME_TABLE}
84+
WHERE
85+
site_id = {websiteId:String}
86+
AND timestamp >= toDateTime({startDate:String})
87+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
88+
GROUP BY date
89+
ORDER BY date ASC
90+
`,
91+
params: { websiteId, startDate, endDate },
92+
};
93+
},
94+
timeField: "timestamp",
95+
customizable: true,
96+
},
97+
98+
uptime_status_breakdown: {
99+
customSql: (websiteId: string, startDate: string, endDate: string) => ({
100+
sql: `
101+
SELECT
102+
status,
103+
http_code,
104+
COUNT(*) as count,
105+
round((COUNT(*) / sum(COUNT(*)) OVER ()) * 100, 2) as percentage
106+
FROM ${UPTIME_TABLE}
107+
WHERE
108+
site_id = {websiteId:String}
109+
AND timestamp >= toDateTime({startDate:String})
110+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
111+
GROUP BY status, http_code
112+
ORDER BY count DESC
113+
`,
114+
params: { websiteId, startDate, endDate },
115+
}),
116+
timeField: "timestamp",
117+
customizable: false,
118+
},
119+
120+
uptime_recent_checks: {
121+
customSql: (
122+
websiteId: string,
123+
startDate: string,
124+
endDate: string,
125+
_filters?: Filter[],
126+
_granularity?: string,
127+
_limit?: number
128+
) => {
129+
const limit = _limit ?? 50;
130+
return {
131+
sql: `
132+
SELECT
133+
timestamp,
134+
url,
135+
status,
136+
http_code,
137+
ttfb_ms,
138+
total_ms,
139+
probe_region,
140+
probe_ip,
141+
ssl_valid,
142+
error
143+
FROM ${UPTIME_TABLE}
144+
WHERE
145+
site_id = {websiteId:String}
146+
AND timestamp >= toDateTime({startDate:String})
147+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
148+
ORDER BY timestamp DESC
149+
LIMIT {limit:UInt32}
150+
`,
151+
params: { websiteId, startDate, endDate, limit },
152+
};
153+
},
154+
timeField: "timestamp",
155+
customizable: true,
156+
},
157+
158+
uptime_response_time_trends: {
159+
customSql: (
160+
websiteId: string,
161+
startDate: string,
162+
endDate: string,
163+
_filters?: Filter[],
164+
_granularity?: string
165+
) => {
166+
const granularity = _granularity ?? "hour";
167+
const timeGroup =
168+
granularity === "minute"
169+
? "toStartOfMinute(timestamp)"
170+
: granularity === "hour"
171+
? "toStartOfHour(timestamp)"
172+
: granularity === "day"
173+
? "toDate(timestamp)"
174+
: "toStartOfHour(timestamp)";
175+
176+
return {
177+
sql: `
178+
SELECT
179+
${timeGroup} as date,
180+
avg(total_ms) as avg_response_time,
181+
quantile(0.50)(total_ms) as p50_response_time,
182+
quantile(0.75)(total_ms) as p75_response_time,
183+
quantile(0.90)(total_ms) as p90_response_time,
184+
quantile(0.95)(total_ms) as p95_response_time,
185+
quantile(0.99)(total_ms) as p99_response_time,
186+
min(total_ms) as min_response_time,
187+
max(total_ms) as max_response_time,
188+
avg(ttfb_ms) as avg_ttfb
189+
FROM ${UPTIME_TABLE}
190+
WHERE
191+
site_id = {websiteId:String}
192+
AND timestamp >= toDateTime({startDate:String})
193+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
194+
AND status = 1
195+
GROUP BY date
196+
ORDER BY date ASC
197+
`,
198+
params: { websiteId, startDate, endDate },
199+
};
200+
},
201+
timeField: "timestamp",
202+
customizable: true,
203+
},
204+
205+
uptime_ssl_status: {
206+
customSql: (websiteId: string, startDate: string, endDate: string) => ({
207+
sql: `
208+
SELECT
209+
any(ssl_expiry) as ssl_expiry,
210+
min(ssl_valid) as ssl_valid,
211+
COUNT(*) as total_checks,
212+
sum(CASE WHEN ssl_valid = 0 THEN 1 ELSE 0 END) as invalid_ssl_checks
213+
FROM ${UPTIME_TABLE}
214+
WHERE
215+
site_id = {websiteId:String}
216+
AND timestamp >= toDateTime({startDate:String})
217+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
218+
AND ssl_expiry IS NOT NULL
219+
GROUP BY site_id
220+
`,
221+
params: { websiteId, startDate, endDate },
222+
}),
223+
timeField: "timestamp",
224+
customizable: false,
225+
},
226+
227+
uptime_by_region: {
228+
customSql: (websiteId: string, startDate: string, endDate: string) => ({
229+
sql: `
230+
SELECT
231+
probe_region as region,
232+
COUNT(*) as total_checks,
233+
sum(status) as successful_checks,
234+
COUNT(*) - sum(status) as failed_checks,
235+
round((sum(status) / COUNT(*)) * 100, 2) as uptime_percentage,
236+
avg(total_ms) as avg_response_time,
237+
quantile(0.95)(total_ms) as p95_response_time
238+
FROM ${UPTIME_TABLE}
239+
WHERE
240+
site_id = {websiteId:String}
241+
AND timestamp >= toDateTime({startDate:String})
242+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
243+
GROUP BY probe_region
244+
ORDER BY total_checks DESC
245+
`,
246+
params: { websiteId, startDate, endDate },
247+
}),
248+
timeField: "timestamp",
249+
customizable: false,
250+
},
251+
};

apps/api/src/types/tables.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
CustomOutgoingLink,
77
ErrorHourlyAggregate,
88
ErrorSpanRow,
9+
UptimeMonitor,
910
WebVitalsHourlyAggregate,
1011
WebVitalsSpan,
1112
} from "@databuddy/db";
@@ -20,6 +21,7 @@ export const Analytics = {
2021
custom_events_hourly: "analytics.custom_events_hourly",
2122
blocked_traffic: "analytics.blocked_traffic",
2223
outgoing_links: "analytics.outgoing_links",
24+
uptime_monitor: "uptime.uptime_monitor",
2325
} as const;
2426

2527
export type AnalyticsTable = (typeof Analytics)[keyof typeof Analytics];
@@ -34,4 +36,5 @@ export type TableFieldsMap = {
3436
"analytics.custom_events_hourly": keyof CustomEventsHourlyAggregate;
3537
"analytics.blocked_traffic": keyof BlockedTraffic;
3638
"analytics.outgoing_links": keyof CustomOutgoingLink;
39+
"uptime.uptime_monitor": keyof UptimeMonitor;
3740
};

0 commit comments

Comments
 (0)