Write a custom sink¶
The trait is two methods, one of which has a default. Implementing it is the supported way to reach a destination the built-ins do not cover, to buffer, or to send to more than one place.
Implement the trait¶
use async_trait::async_trait;
use rtb_telemetry::{Event, TelemetryError, TelemetrySink};
pub struct StdoutSink;
#[async_trait]
impl TelemetrySink for StdoutSink {
async fn emit(&self, event: &Event) -> Result<(), TelemetryError> {
let line = serde_json::to_string(&event.redacted())
.map_err(|e| TelemetryError::Serde(e.to_string()))?;
println!("{line}");
Ok(())
}
}
You need async-trait in your own Cargo.toml — the trait is declared
with #[async_trait], so implementations must carry the attribute too.
emit takes &self, so any state your sink needs is behind a Mutex,
a channel or an atomic. The trait requires Send + Sync + 'static, which
your type will satisfy automatically if its fields do.
Call redacted() if the event leaves the process¶
Nothing calls it for you. event.redacted() returns a clone with args
and err_msg scrubbed; write that, not the original, whenever the
destination is a file, a socket or anything else outside the program. A
sink that only keeps events in memory for tests does not need it — which
is exactly why MemorySink does not do it.
Fan out to several destinations¶
A context holds one sink, so "file and HTTP" is a sink that owns both:
pub struct FanOutSink {
sinks: Vec<Arc<dyn TelemetrySink>>,
}
#[async_trait]
impl TelemetrySink for FanOutSink {
async fn emit(&self, event: &Event) -> Result<(), TelemetryError> {
let mut first_err = None;
for sink in &self.sinks {
if let Err(err) = sink.emit(event).await {
first_err.get_or_insert(err);
}
}
first_err.map_or(Ok(()), Err)
}
async fn flush(&self) -> Result<(), TelemetryError> {
for sink in &self.sinks {
sink.flush().await?;
}
Ok(())
}
}
The interesting decision is the failure policy, and it is yours to make.
The version above tries every sink and reports the first failure, so a dead
HTTP endpoint does not stop the local file being written. Returning on the
first error instead would be a different, equally defensible choice — but
decide it deliberately rather than inheriting it from a ?.
Buffer, if one-request-per-event is too expensive¶
Nothing in the crate batches for you. A buffering sink collects events in
emit, flushes on a size or time threshold, and does the real work in
flush — which is the method TelemetryContext::flush reaches. Two things
to get right: the buffer needs to be Send + Sync, and something has to
call flush before the process exits, or the buffer dies with it. There is
no Drop hook that flushes for you.
Override flush only if you buffer¶
The default flush returns Ok(()). Leave it alone for a sink that writes
synchronously in emit — an override that does nothing is one more thing
to read.