godspeed/trace.rs
1// SPDX-License-Identifier: Apache-2.0
2//! What this service is doing, and what every service has done: metrics, traces and the log tail.
3//!
4//! # Two halves, and only one of them was reachable
5//!
6//! **Publishing** already worked from `gs`: [`metric`] and [`as_name`] wrap methods the context has
7//! always carried, and the SDK instruments every request and reply on its own. What had no route was
8//! **reading any of it back**. The query side is a wire protocol spoken to the `events` service - an
9//! opcode byte out, a packed reply in - and a program that wanted the numbers had to import
10//! `godspeed_sdk`, hand-assemble the request and decode fixed-width fields itself. That is exactly
11//! the kind of protocol this library exists to hide, the same way [`crate::fs`] hides the filesystem's.
12//!
13//! # Everything here answers with a [`Table`]
14//!
15//! Not a printed grid and not JSON. The table is the truth and a rendering is a view of it
16//! (Commandment III), so a caller sorts, filters or aggregates first and renders once:
17//!
18//! ```ignore
19//! let mut t = gs::trace::metrics(&ctx)?;
20//! t.filter("owner", "=", "fs");
21//! t.sort("value", true);
22//! t.to_grid(&mut sink);
23//! ```
24//!
25//! # The log tail here is a CONVENIENCE, not the log
26//!
27//! Every line [`logs`] returns also went to serial and the kernel ring the moment it was written, by
28//! syscall, before `events` ever saw a copy (CLAUDE.md 11.4). Serial is complete; this window is not.
29//! It cannot show anything printed before `events` started, nor a line dropped when the sink's queue
30//! was briefly full. Reach for it to ask a question at a prompt, never to prove something did not
31//! happen.
32//!
33//! # Bounded, like every other wait in this library
34//!
35//! Each query is one [`crate::call::request_within`] with a short deadline: a `CallDeadline`, so a
36//! service that also SERVES clients can call these without a plain receive swallowing an unrelated
37//! request (CLAUDE.md 8.2). The sink holds its ring in memory, so a healthy answer is immediate and a
38//! slow one means the sink is in trouble - which is usually the reason you are asking.
39
40use godspeed_sdk::ipc::Message;
41use godspeed_sdk::service_context::ServiceContext;
42use godspeed_sdk::trace::{MET_LEN, MET_NAME_LEN, PEER_LEN};
43
44use crate::error::Error;
45use crate::record::{Table, Value};
46
47/// The service that holds the rings: `"events"`.
48///
49/// Named so a program can report which service failed to answer, rather than printing a name it
50/// hard-coded separately and would not update if this one moved.
51pub const SINK: &str = godspeed_sdk::trace::SINK_NAME;
52
53/// How long to wait for the sink. The rings are in memory, so a healthy reply is immediate.
54const ASK_SECS: i64 = 3;
55
56/// Declare the name this service publishes under.
57///
58/// **Call this once at startup if you publish anything at all.** Metrics are keyed by
59/// `(owner, name)`, so every service that never declares one publishes under an empty owner and they
60/// all collide into a single row with their counters interleaving. [`metrics`] renders that owner as
61/// `?` rather than blank, because a blank cell reads as a formatting quirk and this is a wrong number.
62pub fn as_name(ctx: &ServiceContext, name: &str) {
63 ctx.trace_as(name);
64}
65
66/// Publish the current value of a named metric.
67///
68/// A metric is a **set, not an increment**: the sink keeps the last value published under
69/// `(owner, name)`, and it keeps it after the publisher dies - a sample outliving its emitter is the
70/// one useful thing left to learn from a service that is gone.
71///
72/// The name is truncated past [`MET_NAME_LEN`](godspeed_sdk::trace::MET_NAME_LEN) bytes, and a
73/// truncated name is a DIFFERENT name that can silently merge two metrics into one row. The SDK says
74/// so once per service rather than once per sample.
75///
76/// This cannot fail in a way a caller can act on, so it returns nothing: the sink is best-effort by
77/// design, and a dropped sample is retried on the next interval rather than failing the work that
78/// produced it.
79pub fn metric(ctx: &ServiceContext, name: &str, value: u64) {
80 ctx.metric(name, value);
81}
82
83/// Every metric the sink currently holds: `owner`, `metric`, `value`, `age_s`.
84///
85/// `age_s` is seconds since that sample was published, not since it was read. Without it a number
86/// frozen at the moment of a crash is indistinguishable from one being maintained right now, and an
87/// instrument that cannot tell those apart is worse than none.
88///
89/// # Errors
90///
91/// Whatever [`crate::call::request_within`] returns: [`Error::Unreachable`] if the sink could not be
92/// reached, [`Error::Busy`] if its queue was full, [`Error::OutcomeUnknown`] if it did not answer in
93/// time, [`Error::PeerDied`] if it died holding the question. Then [`Error::Failed`] - not
94/// [`Error::Malformed`], which is what the rest of this library uses - if it answered with something
95/// this cannot parse. All of these are reads, so any of them may be asked again.
96pub fn metrics(ctx: &ServiceContext) -> Result<Table, Error> {
97 let reply = ask(ctx, godspeed_sdk::trace::TRACE_OP_METRICS)?;
98 let b = reply.payload_bytes();
99 if b.is_empty() {
100 return Err(Error::Failed);
101 }
102 let n = b[0] as usize;
103 let stride = MET_LEN + 4; // the record, plus the u32 timestamp the sink appends
104 if b.len() < 1 + n * stride {
105 return Err(Error::Failed);
106 }
107
108 let now = ctx.epoch_secs_monotonic() as u32;
109 let mut t = Table::new(&["owner", "metric", "value", "age_s"]);
110 for i in 0..n {
111 let o = 1 + i * stride;
112 let owner = trim(&b[o..o + PEER_LEN]);
113 let name = trim(&b[o + PEER_LEN..o + PEER_LEN + MET_NAME_LEN]);
114 let v = o + PEER_LEN + MET_NAME_LEN;
115 let value = u64::from_le_bytes([
116 b[v], b[v + 1], b[v + 2], b[v + 3], b[v + 4], b[v + 5], b[v + 6], b[v + 7],
117 ]);
118 let at = u32::from_le_bytes([
119 b[o + MET_LEN], b[o + MET_LEN + 1], b[o + MET_LEN + 2], b[o + MET_LEN + 3],
120 ]);
121 // An undeclared publisher reads `?`, never blank - see [`as_name`].
122 let ov = if owner.is_empty() { t.intern(b"?") } else { t.intern(owner) };
123 let nv = t.intern(name);
124 t.add_row(&[ov, nv, Value::Int(value), Value::Int(now.saturating_sub(at) as u64)]);
125 }
126 Ok(t)
127}
128
129/// The tail of what services printed: `owner`, `text`.
130///
131/// Read the module header before relying on this - it is a window, not the log.
132///
133/// The owner is a field, so the service's own `name: ` prefix is stripped from the text; keeping it
134/// would state the same fact twice in one row.
135///
136/// # Errors
137///
138/// As [`metrics`].
139pub fn logs(ctx: &ServiceContext) -> Result<Table, Error> {
140 let reply = ask(ctx, godspeed_sdk::trace::TRACE_OP_LOGS)?;
141 let b = reply.payload_bytes();
142 // A 25-byte header carries the cursor fields a repeated drainer needs (which lines are new, and
143 // whether the window outran it). A one-shot reader wants only the text that follows.
144 const HEADER: usize = 25;
145 if b.len() < HEADER {
146 return Err(Error::Failed);
147 }
148
149 let body = &b[HEADER..];
150 let mut t = Table::new(&["owner", "text"]);
151 let mut start = 0usize;
152 for i in 0..=body.len() {
153 let end = if i == body.len() {
154 body.len()
155 } else if body[i] == b'\n' {
156 i
157 } else {
158 continue;
159 };
160 if end > start {
161 let line = &body[start..end];
162 // The sink separates owner from text with 0x1f (unit separator).
163 let cut = line.iter().position(|&c| c == 0x1f).unwrap_or(line.len());
164 let owner = &line[..cut];
165 let mut text = if cut < line.len() { &line[cut + 1..] } else { &line[..0] };
166 // Drop the self-prefix: `fs` + `fs: disk capacity = ...` is one fact twice.
167 if !owner.is_empty() && text.starts_with(owner) {
168 let rest = &text[owner.len()..];
169 if rest.starts_with(b": ") {
170 text = &rest[2..];
171 }
172 }
173 let ov = if owner.is_empty() { t.intern(b"?") } else { t.intern(owner) };
174 let tv = t.intern(text);
175 t.add_row(&[ov, tv]);
176 }
177 start = end + 1;
178 }
179 Ok(t)
180}
181
182/// How the event ring is doing.
183#[derive(Clone, Copy, PartialEq, Eq, Debug)]
184pub struct Status {
185 /// How many events the ring can hold.
186 pub capacity: u64,
187 /// How many have been recorded since boot.
188 pub recorded: u64,
189 /// How many were **overwritten before anyone read them**. Non-zero means this instrument is
190 /// lossy right now, and anything read from it is a sample rather than a record.
191 pub dropped: u64,
192}
193
194/// Capacity, total recorded, and how many were dropped unread.
195///
196/// The ring lives in the `events` service; the kernel records nothing (CLAUDE.md 11.4). So this
197/// answers for a restartable service, and its counters start again when it restarts.
198///
199/// # Errors
200///
201/// As [`metrics`].
202pub fn status(ctx: &ServiceContext) -> Result<Status, Error> {
203 let reply = ask(ctx, godspeed_sdk::trace::TRACE_OP_STATUS)?;
204 let b = reply.payload_bytes();
205 if b.len() < 24 {
206 return Err(Error::Failed);
207 }
208 let at = |i: usize| -> u64 {
209 u64::from_le_bytes([
210 b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7],
211 ])
212 };
213 Ok(Status { capacity: at(0), recorded: at(8), dropped: at(16) })
214}
215
216/// How an exchange ENDED. One row per exchange, so this is its whole story.
217#[derive(Clone, Copy, PartialEq, Eq, Debug)]
218pub enum Outcome {
219 /// A request went out.
220 Request,
221 /// A reply came back. The exchange closed normally.
222 Reply,
223 /// No reply before the deadline. **The outcome is unknown** - it may still have happened.
224 Timeout,
225 /// The peer died while the exchange was open.
226 PeerLost,
227 /// The peer's queue was full, so the request never left. Nothing happened.
228 QueueFull,
229 /// The user changed their mind. Not a failure.
230 Aborted,
231 /// A kind this library does not know. Newer sink than stdlib, or a corrupt row.
232 Unknown,
233}
234
235impl Outcome {
236 /// Did the exchange fail to get where it was going?
237 ///
238 /// True for [`Outcome::Timeout`], [`Outcome::PeerLost`] and [`Outcome::QueueFull`]. **False for
239 /// [`Outcome::Aborted`]**, which is a person deciding, not a fault - counting it as one turns
240 /// every impatient keypress into a reliability statistic.
241 pub fn is_failure(self) -> bool {
242 matches!(self, Outcome::Timeout | Outcome::PeerLost | Outcome::QueueFull)
243 }
244
245 fn from_kind(k: u8) -> Outcome {
246 use godspeed_sdk::trace as t;
247 match k {
248 t::KIND_REQUEST => Outcome::Request,
249 t::KIND_REPLY => Outcome::Reply,
250 t::KIND_TIMEOUT => Outcome::Timeout,
251 t::KIND_PEER_LOST => Outcome::PeerLost,
252 t::KIND_QUEUE_FULL => Outcome::QueueFull,
253 t::KIND_ABORTED => Outcome::Aborted,
254 _ => Outcome::Unknown,
255 }
256 }
257
258 fn name(self) -> &'static [u8] {
259 match self {
260 Outcome::Request => b"REQUEST",
261 Outcome::Reply => b"REPLY",
262 Outcome::Timeout => b"TIMEOUT",
263 Outcome::PeerLost => b"PEER_LOST",
264 Outcome::QueueFull => b"QUEUE_FULL",
265 Outcome::Aborted => b"ABORTED",
266 Outcome::Unknown => b"?",
267 }
268 }
269}
270
271/// The newest `max` IPC exchanges: `seq`, `sec`, `caller`, `peer`, `op`, `outcome`.
272///
273/// Rows are in ring order, oldest first. What each column is worth knowing about:
274///
275/// - **`seq`** is the EMITTER'S own event number, not a global one, so a dump mixing several services
276/// interleaves several sequences and can look unsorted. It is not. A GAP in one service's numbering
277/// is that service's dropped events.
278/// - **`sec`** is seconds since the oldest row shown. The stored value is an epoch second, which says
279/// nothing alone; the GAP between rows is what a stall looks like.
280/// - **`caller`** is who made the call, as that service DECLARED itself ([`as_name`]). A service
281/// cannot be asked its own name - identity is not ambient - so an undeclared one reads `?`.
282/// - **`op`** is the opcode as a NUMBER. Naming it needs knowledge of the protocol being spoken, and
283/// only the service speaking it has that. See this function's source for why guessing is wrong.
284///
285/// `max` is clamped to what one reply can carry. Ask [`status`] how much history exists.
286///
287/// # Errors
288///
289/// As [`metrics`].
290pub fn events(ctx: &ServiceContext, max: usize) -> Result<Table, Error> {
291 let want = max.min(u8::MAX as usize) as u8;
292 let reply = crate::call::request_within(
293 ctx,
294 SINK,
295 &Message::from_bytes(&[godspeed_sdk::trace::TRACE_OP_DUMP, want]),
296 ASK_SECS,
297 )?;
298 let b = reply.payload_bytes();
299 if b.is_empty() {
300 return Err(Error::Failed);
301 }
302 let n = b[0] as usize;
303 let ev = godspeed_sdk::trace::EV_LEN;
304
305 // Every `sec` is relative to the oldest row, so the column reads as elapsed time rather than as
306 // an epoch second nobody can subtract in their head.
307 let base = if n > 0 && b.len() >= 9 {
308 u32::from_le_bytes([b[5], b[6], b[7], b[8]])
309 } else {
310 0
311 };
312
313 let mut t = Table::new(&["seq", "sec", "caller", "peer", "op", "outcome"]);
314 for i in 0..n {
315 let o = 1 + i * ev;
316 if o + ev > b.len() {
317 break; // a short reply is what we got; report what parsed
318 }
319 let seq = u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
320 let at = u32::from_le_bytes([b[o + 4], b[o + 5], b[o + 6], b[o + 7]]);
321 let caller = trim(&b[o + 8..o + 8 + PEER_LEN]);
322 let peer = trim(&b[o + 8 + PEER_LEN..o + 8 + 2 * PEER_LEN]);
323 let op = b[o + 8 + 2 * PEER_LEN];
324 let outcome = Outcome::from_kind(b[o + 9 + 2 * PEER_LEN]);
325
326 let cv = if caller.is_empty() { t.intern(b"?") } else { t.intern(caller) };
327 let pv = t.intern(peer);
328 let ov = t.intern(outcome.name());
329 t.add_row(&[
330 Value::Int(seq as u64),
331 Value::Int(at.saturating_sub(base) as u64),
332 cv,
333 pv,
334 Value::Int(op as u64),
335 ov,
336 ]);
337 }
338 Ok(t)
339}
340
341/// One bounded question to the sink.
342fn ask(ctx: &ServiceContext, op: u8) -> Result<Message, Error> {
343 crate::call::request_within(ctx, SINK, &Message::from_bytes(&[op]), ASK_SECS)
344}
345
346/// A fixed-width field is NUL-padded; the name ends at the first NUL.
347fn trim(field: &[u8]) -> &[u8] {
348 let n = field.iter().position(|&c| c == 0).unwrap_or(field.len());
349 &field[..n]
350}