Sentry
Errors and messages sent to Sentry, GlitchTip or any Sentry-compatible server as envelope events from a DSN, with level, tags, user, request, release, breadcrumbs and a stack trace, without an SDK.
Version 1.0.0 · 3 functions · 3 crates
Functions
What use sentry.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use sentry_dsn, sentry_message from sentry (Quillo 0.6).
sentry_dsn(dsn: str) → anyor an errorA DSN taken apart: { scheme, public_key, host, path, project_id, envelope_url } (https://<key>@o1.ingest.sentry.io/42).
sentry_message(dsn: str, message: str, level: str, options: map) → anyor an errorSends a message event (level "fatal", "error", "warning", "info" or "debug") to the DSN's project (empty: SENTRY_DSN). options may hold tags, extra, user ({ id, email }), request ({ url, method, headers }, cookies and authorization dropped), release and environment (else SENTRY_RELEASE, SENTRY_ENVIRONMENT), breadcrumbs, fingerprint, transaction. Gives { ok, status, event_id, error, rate_limited }.
sentry_error(dsn: str, error_type: str, value: str, options: map) → anyor an errorSends an error event: error_type ("PaymentFailed") and value (its message) show as the issue's title; options as for sentry_message, plus stacktrace, a list of frames { function, filename, lineno } from the outermost call in, and level ("error" unless given). Events with the same type and stack trace group into one issue.
Get it
$ quillo install sentry # sentry.qwer into this folder$ quillo install sentry --example # and its example, sentry.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/sentry.qwer$ curl -O https://quillolang.org/plugins/files/sentry.qweThen, in your program, all of it or only what you call:
use sentry.qweruse sentry_dsn, sentry_message from sentry // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
ureq = "2"rand = "0.8"serde_json = { version = "1", features = ["preserve_order"] }Example
// A shop's failed payments and slow imports reported to Sentry, with the request and the user,// so the team sees them grouped as issues.//// SENTRY_DSN=https://<key>@o1.ingest.sentry.io/42 quillo run sentry.qwe then open http://localhost:8080/pay//// Without SENTRY_DSN, a DSN for SINK_URL (a stand-in server, in tests) is used.use "sentry.qwer"let DSN = env("SENTRY_DSN", null) ?? replace(env("SINK_URL", "http://127.0.0.1:9"), "://", "://public123@") + "/42"route GET /pay { // The payment provider said no: report it, and answer the customer. let r = sentry_error(DSN, "PaymentDeclined", "card_declined: insufficient funds for order A-1042", { tags: { provider: "stripe", order: "A-1042" }, user: { id: "u42", email: "ada@example.org" }, request: { url: "https://shop.example.com/pay", method: "POST", headers: request.headers }, release: "shop@2.4.0", environment: "production", stacktrace: [{ function: "route POST /pay", filename: "shop.qwe", lineno: 88 }, { function: "charge", filename: "payments.qwe", lineno: 41 }], breadcrumbs: [{ category: "cart", message: "3 items, €52.90", level: "info" }], })? { reported: r.ok, status: r.status, event_id_length: len(r.event_id) }}route GET /slow = sentry_message(DSN, "nightly import took 14 minutes", "warning", { tags: { job: "import" } })?route GET /dsn = sentry_dsn("https://abc123@o450000.ingest.sentry.io/4500000000000001")?$ quillo run sentry.qwe$ curl 'localhost:8080/pay'$ curl 'localhost:8080/slow'$ curl 'localhost:8080/dsn'It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.
Source
//! Errors and messages sent to Sentry (or GlitchTip, or any Sentry-compatible server) as envelope events from a DSN: level, tags, user, request, release, environment, breadcrumbs and a stack trace, without an SDK.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! rand = "0.8"//! serde_json = { version = "1", features = ["preserve_order"] }use rand::RngCore;use serde_json::{json, Map, Value};fn now() -> f64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0.0, |d| d.as_secs_f64())}fn event_id() -> String { let mut b = [0u8; 16]; rand::thread_rng().fill_bytes(&mut b); b.iter().map(|x| format!("{:02x}", x)).collect()}fn dsn_or_env(dsn: &str) -> Result<String, String> { if !dsn.trim().is_empty() { return Ok(dsn.trim().to_string()); } std::env::var("SENTRY_DSN").ok().filter(|d| !d.is_empty()).ok_or_else(|| "no DSN, and SENTRY_DSN is not set".into())}/// A DSN taken apart: `{ scheme, public_key, host, path, project_id, envelope_url }` (`https://<key>@o1.ingest.sentry.io/42`).pub fn sentry_dsn(dsn: &str) -> Result<Value, String> { let d = dsn_or_env(dsn)?; let (scheme, rest) = d.split_once("://").ok_or("a DSN looks like https://<key>@<host>/<project>")?; let (key, rest) = rest.split_once('@').ok_or("a DSN has the public key before @")?; let key = key.split(':').next().unwrap_or(key); let (host_path, project) = rest.rsplit_once('/').ok_or("a DSN ends with /<project id>")?; let (host, path) = host_path.split_once('/').map_or((host_path, String::new()), |(h, p)| (h, format!("/{}", p))); if project.is_empty() || key.is_empty() { return Err("a DSN needs a public key and a project id".into()); } Ok(json!({ "scheme": scheme, "public_key": key, "host": host, "path": path, "project_id": project, "envelope_url": format!("{}://{}{}/api/{}/envelope/", scheme, host, path, project), }))}fn send(dsn: &str, event: Value) -> Result<Value, String> { let d = sentry_dsn(dsn)?; let id = event["event_id"].as_str().unwrap_or_default().to_string(); let header = json!({ "event_id": id, "dsn": dsn_or_env(dsn)?, "sent_at": iso(now()) }); let body = format!("{}\n{}\n{}\n", header, json!({ "type": "event", "content_type": "application/json" }), event); let auth = format!("Sentry sentry_version=7, sentry_client=quillo-sentry/1.0, sentry_key={}", d["public_key"].as_str().unwrap_or("")); let res = ureq::post(d["envelope_url"].as_str().unwrap_or("")) .timeout(std::time::Duration::from_secs(10)) .set("Content-Type", "application/x-sentry-envelope") .set("X-Sentry-Auth", &auth) .send_string(&body); let (status, text) = match res { Ok(r) => (r.status(), r.into_string().unwrap_or_default()), Err(ureq::Error::Status(c, r)) => (c, r.into_string().unwrap_or_default()), Err(e) => return Err(format!("Sentry: {}", e)), }; let ok = (200..300).contains(&status); Ok(json!({ "ok": ok, "status": status, "event_id": id, "error": if ok { Value::Null } else { json!(text.chars().take(300).collect::<String>()) }, "rate_limited": status == 429 }))}fn iso(t: f64) -> String { let secs = t as i64; let (days, s) = (secs.div_euclid(86400), secs.rem_euclid(86400)); let z = days + 719468; let era = z.div_euclid(146097); let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 }; format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, s / 3600, s % 3600 / 60, s % 60)}fn base_event(level: &str, options: &Map<String, Value>) -> Result<Value, String> { if !["fatal", "error", "warning", "info", "debug"].contains(&level) { return Err(format!("level `{}` is fatal, error, warning, info or debug", level)); } let mut e = json!({ "event_id": event_id(), "timestamp": now(), "platform": "other", "level": level, "logger": options.get("logger").cloned().unwrap_or(json!("quillo")), "sdk": { "name": "quillo-sentry", "version": "1.0.0" }, }); if let Some(h) = options.get("server_name").cloned().or_else(|| std::env::var("HOSTNAME").ok().filter(|h| !h.is_empty()).map(Value::String)) { e["server_name"] = h; } let env_or = |k: &str, var: &str| options.get(k).cloned().or_else(|| std::env::var(var).ok().map(Value::String)); if let Some(r) = env_or("release", "SENTRY_RELEASE") { e["release"] = r; } if let Some(r) = env_or("environment", "SENTRY_ENVIRONMENT") { e["environment"] = r; } for k in ["tags", "extra", "user", "contexts", "fingerprint", "transaction"] { if let Some(v) = options.get(k) { e[k] = v.clone(); } } if let Some(r) = options.get("request") { // Only what helps to reproduce; cookies and authorization never leave. let mut req = r.clone(); if let Some(h) = req.get_mut("headers").and_then(|h| h.as_object_mut()) { h.retain(|k, _| !["cookie", "authorization", "x-api-key"].contains(&k.to_lowercase().as_str())); } e["request"] = req; } if let Some(b) = options.get("breadcrumbs").and_then(|b| b.as_array()) { e["breadcrumbs"] = json!({ "values": b }); } Ok(e)}/// Sends a message event (`level` "fatal", "error", "warning", "info" or "debug") to the DSN's project (empty: SENTRY_DSN)./// `options` may hold `tags`, `extra`, `user` (`{ id, email }`), `request` (`{ url, method, headers }`, cookies and authorization/// dropped), `release` and `environment` (else SENTRY_RELEASE, SENTRY_ENVIRONMENT), `breadcrumbs`, `fingerprint`, `transaction`./// Gives `{ ok, status, event_id, error, rate_limited }`.pub fn sentry_message(dsn: &str, message: &str, level: &str, options: std::collections::HashMap<String, serde_json::Value>) -> Result<Value, String> { let o: Map<String, Value> = options.into_iter().collect(); let mut e = base_event(level, &o)?; e["message"] = json!({ "formatted": message }); send(dsn, e)}/// Sends an error event: `error_type` ("PaymentFailed") and `value` (its message) show as the issue's title; `options` as for/// `sentry_message`, plus `stacktrace`, a list of frames `{ function, filename, lineno }` from the outermost call in, and `level`/// ("error" unless given). Events with the same type and stack trace group into one issue.pub fn sentry_error(dsn: &str, error_type: &str, value: &str, options: std::collections::HashMap<String, serde_json::Value>) -> Result<Value, String> { let o: Map<String, Value> = options.into_iter().collect(); let level = o.get("level").and_then(|l| l.as_str()).unwrap_or("error").to_string(); let mut e = base_event(&level, &o)?; let mut ex = json!({ "type": error_type, "value": value, "mechanism": { "type": "generic", "handled": true } }); if let Some(frames) = o.get("stacktrace").and_then(|s| s.as_array()) { ex["stacktrace"] = json!({ "frames": frames.iter().map(|f| { let mut f = f.clone(); f["in_app"] = json!(true); f }).collect::<Vec<_>>() }); } e["exception"] = json!({ "values": [ex] }); send(dsn, e)}Related
otlp.qwer · 1.0.0Traces, logs and metrics sent as OTLP/HTTP JSON to a collector, Grafana, Honeycomb or Jaeger: spans with parents, attributes and status, W3C traceparent headers made and read, logs tied to their trace, and gauges and counters.7 views · 7 downloadsRolling windows rolling_windows.qwer · 1.0.0Moving counts, sums, averages and rates over time windows, kept in memory by name: requests a minute, error rates, this hour against the last, and moving averages over rows.8 views · 8 downloadsDatadog datadog.qwer · 1.0.0Report to Datadog from a program: metrics through the v2 series API with names and timestamps checked, events and service checks, tags tidied as Datadog keeps them, and logs batched and gzipped within the intake's limits of 1000 entries and 5 MB.3 views · 8 downloadsntfy push ntfy.qwer · 1.0.0Push notifications to phones and desktops through ntfy.sh or your own ntfy server: title, priority, tags and emoji, a link, action buttons, attachments, Markdown and delayed delivery, with a token or a password.8 views · 7 downloadsPagerDuty pagerduty.qwer · 1.0.0Page people from a program: Events API v2 alerts triggered, acknowledged and resolved with stable dedup keys so repeats join one incident, change events for deploys, retries when PagerDuty asks for them, and the REST API's incidents listed, updated and noted.3 views · 6 downloadsGrafana grafana.qwer · 1.0.0Save, find and remove Grafana dashboards and folders, mark deploys with annotations, run data-source queries with their frames read as rows, keep alert rules, and make service accounts and tokens, with version conflicts and a query's own failure turned into clear errors.3 views · 7 downloadsThread 0 posts
On Sentry: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.