split timings, remove useless kvs
This commit is contained in:
parent
a6d0596c9e
commit
8c3cc3739a
|
|
@ -14,14 +14,11 @@ const StatusPageBody = t.Object({
|
|||
index_search: t.Optional(t.Boolean()),
|
||||
show_powered_by: t.Optional(t.Boolean()),
|
||||
show_response_time: t.Optional(t.Boolean()),
|
||||
show_cert_expiry: t.Optional(t.Boolean()),
|
||||
bar_frequency: t.Optional(BarFrequency),
|
||||
bar_count: t.Optional(t.Number({ minimum: 1, maximum: 180 })),
|
||||
custom_domain: t.Optional(t.Nullable(t.String({ maxLength: 253 }))),
|
||||
custom_css: t.Optional(t.Nullable(t.String({ maxLength: 50_000 }))),
|
||||
footer_text: t.Optional(t.Nullable(t.String({ maxLength: 5000 }))),
|
||||
og_image_url: t.Optional(t.Nullable(t.String({ maxLength: 2048 }))),
|
||||
analytics_html: t.Optional(t.Nullable(t.String({ maxLength: 5000 }))),
|
||||
auto_refresh_s: t.Optional(t.Number({ minimum: 10, maximum: 3600 })),
|
||||
groups: t.Optional(t.Array(t.Object({
|
||||
name: t.String({ minLength: 1, maxLength: 200 }),
|
||||
|
|
@ -128,18 +125,17 @@ export const statusPages = new Elysia({ prefix: "/pages" })
|
|||
[row] = await sql`
|
||||
INSERT INTO status_pages (
|
||||
account_id, slug, title, description, theme, password_hash, index_search,
|
||||
show_powered_by, show_response_time, show_cert_expiry,
|
||||
show_powered_by, show_response_time,
|
||||
bar_frequency, bar_count,
|
||||
custom_domain, custom_css, footer_text, og_image_url, analytics_html, auto_refresh_s
|
||||
custom_domain, custom_css, footer_text, auto_refresh_s
|
||||
)
|
||||
VALUES (
|
||||
${accountId}, ${body.slug}, ${body.title}, ${body.description ?? null},
|
||||
${body.theme ?? 'auto'}, ${password_hash}, ${body.index_search ?? true},
|
||||
${body.show_powered_by ?? true}, ${body.show_response_time ?? true},
|
||||
${body.show_cert_expiry ?? false},
|
||||
${body.bar_frequency ?? 'daily'}, ${body.bar_count ?? 90},
|
||||
${body.custom_domain || null}, ${css}, ${body.footer_text ?? null}, ${body.og_image_url ?? null},
|
||||
${body.analytics_html ?? null}, ${body.auto_refresh_s ?? 60}
|
||||
${body.custom_domain || null}, ${css}, ${body.footer_text ?? null},
|
||||
${body.auto_refresh_s ?? 60}
|
||||
)
|
||||
RETURNING *
|
||||
`;
|
||||
|
|
@ -191,14 +187,11 @@ export const statusPages = new Elysia({ prefix: "/pages" })
|
|||
index_search = COALESCE(${body.index_search ?? null}, index_search),
|
||||
show_powered_by = COALESCE(${body.show_powered_by ?? null}, show_powered_by),
|
||||
show_response_time = COALESCE(${body.show_response_time ?? null}, show_response_time),
|
||||
show_cert_expiry = COALESCE(${body.show_cert_expiry ?? null}, show_cert_expiry),
|
||||
bar_frequency = COALESCE(${body.bar_frequency ?? null}, bar_frequency),
|
||||
bar_count = COALESCE(${body.bar_count ?? null}, bar_count),
|
||||
custom_domain = CASE WHEN ${body.custom_domain !== undefined} THEN ${body.custom_domain || null} ELSE custom_domain END,
|
||||
custom_css = CASE WHEN ${body.custom_css !== undefined} THEN ${css} ELSE custom_css END,
|
||||
footer_text = COALESCE(${body.footer_text ?? null}, footer_text),
|
||||
og_image_url = COALESCE(${body.og_image_url ?? null}, og_image_url),
|
||||
analytics_html = COALESCE(${body.analytics_html ?? null}, analytics_html),
|
||||
auto_refresh_s = COALESCE(${body.auto_refresh_s ?? null}, auto_refresh_s),
|
||||
updated_at = now()
|
||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
});
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase();
|
||||
let timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000));
|
||||
let is_https = monitor.url.starts_with("https://");
|
||||
|
|
@ -170,7 +169,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
let req_body = monitor.request_body.clone();
|
||||
let max_redirects = monitor.max_redirects;
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(u16, HashMap<String, String>, String), String>>();
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(u16, HashMap<String, String>, String, u64, u64), String>>();
|
||||
std::thread::spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
run_check_blocking(&url, &method, req_headers.as_ref(), req_body.as_deref(), timeout, max_redirects)
|
||||
|
|
@ -187,10 +186,9 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
.and_then(|r| r.map_err(|_| "check thread dropped".to_string()))
|
||||
.unwrap_or_else(|e| Err(e));
|
||||
|
||||
let latency_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
match result {
|
||||
Err(ref e) => {
|
||||
let latency_ms = start.elapsed().as_millis() as u64;
|
||||
debug!("{} check error: {e}", monitor.url);
|
||||
PingResult {
|
||||
monitor_id: monitor.id.clone(),
|
||||
|
|
@ -209,7 +207,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
run_id: Some(run_id.to_string()),
|
||||
}
|
||||
},
|
||||
Ok((status, headers, body)) => {
|
||||
Ok((status, headers, body, total_ms, dns_ms)) => {
|
||||
|
||||
let cert_handle = if is_https {
|
||||
let cert_url = monitor.url.clone();
|
||||
|
|
@ -233,7 +231,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
status,
|
||||
body: body.clone(),
|
||||
headers: headers.clone(),
|
||||
latency_ms: Some(latency_ms),
|
||||
latency_ms: Some(total_ms.saturating_sub(dns_ms)),
|
||||
cert_expiry_days: None,
|
||||
cert_issuer: None,
|
||||
};
|
||||
|
|
@ -253,14 +251,18 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
None => None,
|
||||
};
|
||||
let cert_expiry_days = cert_info.as_ref().map(|c| c.expiry_days);
|
||||
let tls_ms = cert_info.as_ref().map(|c| c.tls_ms).unwrap_or(0);
|
||||
let cert_issuer = cert_info.map(|c| c.issuer);
|
||||
|
||||
// Subtract DNS and TLS time from total to get server response time only
|
||||
let latency_ms = total_ms.saturating_sub(dns_ms).saturating_sub(tls_ms);
|
||||
|
||||
let meta = json!({
|
||||
"headers": headers,
|
||||
"body_preview": &body[..body.len().min(25_000)],
|
||||
});
|
||||
|
||||
debug!("{} → {status} {latency_ms}ms up={up}", monitor.url);
|
||||
debug!("{} → {status} {latency_ms}ms (total={total_ms} dns={dns_ms} tls={tls_ms}) up={up}", monitor.url);
|
||||
|
||||
PingResult {
|
||||
monitor_id: monitor.id.clone(),
|
||||
|
|
@ -282,6 +284,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
}
|
||||
}
|
||||
|
||||
// Returns (status, headers, body, total_ms, dns_ms)
|
||||
fn run_check_blocking(
|
||||
url: &str,
|
||||
method: &str,
|
||||
|
|
@ -289,7 +292,18 @@ fn run_check_blocking(
|
|||
body: Option<&str>,
|
||||
timeout: std::time::Duration,
|
||||
max_redirects: u32,
|
||||
) -> Result<(u16, HashMap<String, String>, String), String> {
|
||||
) -> Result<(u16, HashMap<String, String>, String, u64, u64), String> {
|
||||
// Measure DNS resolution time separately (lookup only, no connection)
|
||||
let dns_ms = {
|
||||
let parsed = reqwest::Url::parse(url).map_err(|e| e.to_string())?;
|
||||
let host = parsed.host_str().unwrap_or("");
|
||||
let port = parsed.port().unwrap_or(if parsed.scheme() == "https" { 443 } else { 80 });
|
||||
let addr = format!("{host}:{port}");
|
||||
let dns_start = Instant::now();
|
||||
let _ = std::net::ToSocketAddrs::to_socket_addrs(&addr as &str);
|
||||
dns_start.elapsed().as_millis() as u64
|
||||
};
|
||||
|
||||
let root_certs = ROOT_CERTS.with(|c| Arc::clone(c));
|
||||
|
||||
let tls = ureq::tls::TlsConfig::builder()
|
||||
|
|
@ -306,6 +320,8 @@ fn run_check_blocking(
|
|||
.build()
|
||||
.new_agent();
|
||||
|
||||
let request_start = Instant::now();
|
||||
|
||||
let mut builder = ureq::http::Request::builder()
|
||||
.method(method)
|
||||
.uri(url);
|
||||
|
|
@ -368,12 +384,14 @@ fn run_check_blocking(
|
|||
}
|
||||
};
|
||||
|
||||
Ok((status, resp_headers, body_out))
|
||||
let total_ms = request_start.elapsed().as_millis() as u64;
|
||||
Ok((status, resp_headers, body_out, total_ms, dns_ms))
|
||||
}
|
||||
|
||||
struct CertInfo {
|
||||
expiry_days: i64,
|
||||
issuer: String,
|
||||
tls_ms: u64,
|
||||
}
|
||||
|
||||
async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
|
||||
|
|
@ -398,7 +416,9 @@ async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
|
|||
let server_name = ServerName::try_from(host.to_string())?;
|
||||
|
||||
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
|
||||
let tls_start = Instant::now();
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let tls_ms = tls_start.elapsed().as_millis() as u64;
|
||||
|
||||
let (_, conn) = tls_stream.get_ref();
|
||||
let certs = conn.peer_certificates().unwrap_or(&[]);
|
||||
|
|
@ -412,7 +432,7 @@ async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
|
|||
.as_secs() as i64;
|
||||
let days = (not_after - now) / 86400;
|
||||
let issuer = cert.issuer().to_string();
|
||||
return Ok(Some(CertInfo { expiry_days: days, issuer }));
|
||||
return Ok(Some(CertInfo { expiry_days: days, issuer, tls_ms }));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
|
|
|
|||
|
|
@ -129,14 +129,11 @@ export async function migrate(sql: any) {
|
|||
index_search BOOLEAN NOT NULL DEFAULT true,
|
||||
show_powered_by BOOLEAN NOT NULL DEFAULT true,
|
||||
show_response_time BOOLEAN NOT NULL DEFAULT true,
|
||||
show_cert_expiry BOOLEAN NOT NULL DEFAULT false,
|
||||
bar_frequency TEXT NOT NULL DEFAULT 'daily',
|
||||
bar_count INTEGER NOT NULL DEFAULT 90,
|
||||
custom_domain TEXT UNIQUE,
|
||||
custom_css TEXT,
|
||||
footer_text TEXT,
|
||||
og_image_url TEXT,
|
||||
analytics_html TEXT,
|
||||
auto_refresh_s INTEGER NOT NULL DEFAULT 60,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@ export interface StatusPageRow {
|
|||
index_search: boolean;
|
||||
show_powered_by: boolean;
|
||||
show_response_time:boolean;
|
||||
show_cert_expiry: boolean;
|
||||
|
||||
bar_frequency: BucketType;
|
||||
bar_count: number;
|
||||
custom_domain: string | null;
|
||||
custom_css: string | null;
|
||||
footer_text: string | null;
|
||||
og_image_url: string | null;
|
||||
analytics_html: string | null;
|
||||
auto_refresh_s: number;
|
||||
}
|
||||
|
||||
|
|
@ -498,11 +496,6 @@ export async function loadMonitorDetail(slug: string, monitorId: string): Promis
|
|||
// The shape we actually expose to anonymous visitors. Computed by stripping
|
||||
// internal IDs and any field a public consumer doesn't need from the row
|
||||
// types - see redactPageForPublic / redactGroupsAndMonitors below.
|
||||
//
|
||||
// custom_css and analytics_html are kept here even though they're noisy to
|
||||
// JSON consumers, because the HTML render reads from this same object - and
|
||||
// they're already publicly visible in the rendered HTML, so dropping them
|
||||
// from JSON wouldn't actually add any privacy.
|
||||
export interface PublicPageView {
|
||||
slug: string;
|
||||
title: string;
|
||||
|
|
@ -511,13 +504,9 @@ export interface PublicPageView {
|
|||
index_search: boolean;
|
||||
show_powered_by: boolean;
|
||||
show_response_time: boolean;
|
||||
show_cert_expiry: boolean;
|
||||
bar_frequency: BucketType;
|
||||
bar_count: number;
|
||||
custom_css: string | null;
|
||||
footer_text: string | null;
|
||||
og_image_url: string | null;
|
||||
analytics_html: string | null;
|
||||
auto_refresh_s: number;
|
||||
has_password: boolean;
|
||||
}
|
||||
|
|
@ -549,13 +538,9 @@ function redactPageForPublic(p: StatusPageRow): PublicPageView {
|
|||
index_search: p.index_search,
|
||||
show_powered_by: p.show_powered_by,
|
||||
show_response_time: p.show_response_time,
|
||||
show_cert_expiry: p.show_cert_expiry,
|
||||
bar_frequency: p.bar_frequency,
|
||||
bar_count: p.bar_count,
|
||||
custom_css: p.custom_css,
|
||||
footer_text: p.footer_text,
|
||||
og_image_url: p.og_image_url,
|
||||
analytics_html: p.analytics_html,
|
||||
auto_refresh_s: p.auto_refresh_s,
|
||||
has_password: !!p.password_hash,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#0a0a0a"/>
|
||||
<text x="3" y="23" font-family="system-ui, sans-serif" font-weight="700" font-size="18" fill="#e5e7eb">P</text>
|
||||
<text x="14" y="23" font-family="system-ui, sans-serif" font-weight="700" font-size="18" fill="#3b82f6">Q</text>
|
||||
<circle cx="26" cy="6" r="3" fill="#22c55e">
|
||||
<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
|
|
@ -105,12 +105,11 @@
|
|||
<% if (!page.index_search) { %><meta name="robots" content="noindex,nofollow"><% } %>
|
||||
<meta property="og:title" content="<%= page.title %>">
|
||||
<% if (page.description) { %><meta property="og:description" content="<%= page.description %>"><% } %>
|
||||
<% if (page.og_image_url) { %><meta property="og:image" content="<%= page.og_image_url %>"><% } %>
|
||||
<link rel="icon" href="/_static/favicon.svg" type="image/svg+xml">
|
||||
<link rel="alternate" type="application/rss+xml" title="<%= page.title %> incidents" href="/<%= page.slug %>.rss">
|
||||
<link rel="manifest" href="/<%= page.slug %>/manifest.json">
|
||||
<link rel="stylesheet" href="/_static/app.css?v=<%= it.appCssHash %>">
|
||||
<% if (page.custom_css) { %><style><%~ page.custom_css %></style><% } %>
|
||||
<% if (page.analytics_html) { %><%~ page.analytics_html %><% } %>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -738,11 +738,11 @@ export const dashboard = new Elysia()
|
|||
bar_frequency: b.bar_frequency || "daily",
|
||||
bar_count: Number(b.bar_count) || 90,
|
||||
show_response_time: !!b.show_response_time,
|
||||
show_cert_expiry: !!b.show_cert_expiry,
|
||||
|
||||
show_powered_by: !!b.show_powered_by,
|
||||
index_search: !!b.index_search,
|
||||
auto_refresh_s: Number(b.auto_refresh_s) || 60,
|
||||
og_image_url: b.og_image_url || null,
|
||||
|
||||
password: b.password || undefined,
|
||||
custom_domain: b.custom_domain || null,
|
||||
custom_css: b.custom_css || null,
|
||||
|
|
@ -771,11 +771,9 @@ export const dashboard = new Elysia()
|
|||
bar_frequency: b.bar_frequency || "daily",
|
||||
bar_count: Number(b.bar_count) || 90,
|
||||
show_response_time: !!b.show_response_time,
|
||||
show_cert_expiry: !!b.show_cert_expiry,
|
||||
show_powered_by: !!b.show_powered_by,
|
||||
index_search: !!b.index_search,
|
||||
auto_refresh_s: Number(b.auto_refresh_s) || 60,
|
||||
og_image_url: b.og_image_url || null,
|
||||
custom_domain: b.custom_domain || null,
|
||||
custom_css: b.custom_css || null,
|
||||
footer_text: b.footer_text || null,
|
||||
|
|
|
|||
|
|
@ -300,12 +300,9 @@ Content-Type: application/json
|
|||
<tr><td>custom_domain</td><td>string?</td><td>Custom domain for the page (e.g. <code>status.example.com</code>). CNAME to the status server IP. SSL is automatic.</td></tr>
|
||||
<tr><td>custom_css</td><td>string?</td><td>Custom CSS injected after the default stylesheet (up to 50KB).</td></tr>
|
||||
<tr><td>footer_text</td><td>string?</td><td>Text shown in the page footer (up to 5000 chars).</td></tr>
|
||||
<tr><td>og_image_url</td><td>string?</td><td>OpenGraph image URL for social previews.</td></tr>
|
||||
<tr><td>analytics_html</td><td>string?</td><td>Raw HTML injected in the head (e.g. analytics snippet).</td></tr>
|
||||
<tr><td>index_search</td><td>boolean?</td><td>Allow search engine indexing. Default <code>true</code>.</td></tr>
|
||||
<tr><td>show_powered_by</td><td>boolean?</td><td>Show "Powered by PingQL" footer. Default <code>true</code>.</td></tr>
|
||||
<tr><td>show_response_time</td><td>boolean?</td><td>Display response time per monitor. Default <code>true</code>.</td></tr>
|
||||
<tr><td>show_cert_expiry</td><td>boolean?</td><td>Show cert expiry info. Default <code>false</code>.</td></tr>
|
||||
<tr><td>groups</td><td>array?</td><td>Groups to organize monitors. Each has <code>name</code> (string) and optional <code>position</code> (number).</td></tr>
|
||||
<tr><td>monitors</td><td>array?</td><td>Monitors to display. See monitor fields below.</td></tr>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -223,10 +223,6 @@
|
|||
<input type="checkbox" name="show_response_time" value="1" <%= (p.show_response_time !== false) ? 'checked' : '' %> class="accent-blue-500">
|
||||
Show response time
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<input type="checkbox" name="show_cert_expiry" value="1" <%= p.show_cert_expiry ? 'checked' : '' %> class="accent-blue-500">
|
||||
Show cert expiry
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<input type="checkbox" name="show_powered_by" value="1" <%= (p.show_powered_by !== false) ? 'checked' : '' %> class="accent-blue-500">
|
||||
Show "Powered by PingQL"
|
||||
|
|
@ -246,11 +242,6 @@
|
|||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm text-gray-400 mb-1.5">OG image URL <span class="text-gray-600">(optional)</span></label>
|
||||
<input name="og_image_url" type="url" value="<%= p.og_image_url || '' %>" placeholder="https://example.com/og.png"
|
||||
class="w-full bg-surface-solid border border-border-subtle rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% const hasPassword = !it.isNew && p.password_hash; %>
|
||||
|
|
|
|||
Loading…
Reference in New Issue