Skip to content

Commit 5817bc0

Browse files
author
Derek
committed
fix: masking layer, coloured output, integration tests
1 parent eeae365 commit 5817bc0

9 files changed

Lines changed: 1504 additions & 52 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ default = ["config", "logger", "metrics", "env", "runtime"]
2626
env = []
2727
runtime = ["dirs"]
2828
config = ["figment", "dotenvy", "serde_yaml_ng", "serde_json", "toml", "dirs", "tracing"]
29-
logger = ["tracing", "tracing-subscriber", "owo-colors"]
29+
logger = ["tracing", "tracing-subscriber", "owo-colors", "serde_json"]
3030
metrics = ["dep:metrics", "metrics-exporter-prometheus", "sysinfo", "tokio"]
3131

3232
# OpenTelemetry support (modern observability)

TODO.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,25 @@
1010

1111
### High Priority
1212

13-
- [ ] Remove stale `hs-rustlib` crate from JFrog `hypersec-cargo-local` registry
1413
- [ ] Update downstream consumers to use `transport-grpc` / `transport-grpc-vector-compat`
1514
- dfe-loader, dfe-archiver, dfe-receiver
1615

1716
### Medium Priority
1817

19-
- [ ] Fix MaskingLayer no-op bug (masking layer never actually redacts output)
2018
- [ ] Fix vault_env integration tests (EnvGuard doesn't clear conflicting VAULT_TOKEN)
21-
- [ ] Implement log output capturing for logger tests
22-
- [ ] Add metrics server graceful shutdown tests
23-
- [ ] Add gRPC transport integration tests (bidirectional client/server)
24-
- [ ] Add Vector compat source/sink integration tests
25-
26-
### Low Priority
27-
28-
- [ ] Add coloured log output for text format (custom FormatEvent with owo-colors)
19+
- [ ] Add Vector compat source/sink integration tests (use fetch-vector.sh from dfe-receiver)
2920

3021
---
3122

3223
## Completed
3324

25+
- [x] Dependency update sweep — all crates to latest, tonic/prost 0.14 migration (v1.8.4)
26+
- [x] Stale hs-rustlib removed from JFrog hypersec-cargo-local and hyperi-cargo-local
27+
- [x] MaskingLayer fixed — writer-based redaction for both JSON and text formats (v1.8.4)
28+
- [x] Logger output capturing tests — 10 tests (JSON, text, filtering, masking)
29+
- [x] Coloured log output — custom FormatEvent with owo-colors colour scheme
30+
- [x] Metrics graceful shutdown tests — 4 tests (shutdown, rapid cycle, render after stop, concurrent)
31+
- [x] gRPC transport integration tests — 8 tests (send/recv, ordering, large payload, compression)
3432
- [x] gRPC transport with Vector wire protocol compatibility (v1.8.0)
3533
- tonic-based gRPC replacing Zenoh transport
3634
- DFE native proto (`dfe.transport.v1`) + vendored Vector proto

build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Protobuf code generation for gRPC transport.
55
// Only runs when transport-grpc or transport-grpc-vector-compat features are enabled.
66

7+
#[allow(clippy::unnecessary_wraps)]
78
fn main() -> Result<(), Box<dyn std::error::Error>> {
89
// DFE native transport proto
910
#[cfg(feature = "transport-grpc")]

src/logger/format.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// Project: hyperi-rustlib
2+
// File: src/logger/format.rs
3+
// Purpose: Coloured log output formatter using owo-colors
4+
// Language: Rust
5+
//
6+
// License: FSL-1.1-ALv2
7+
// Copyright: (c) 2026 HYPERI PTY LIMITED
8+
9+
//! Custom coloured log formatter for terminal output.
10+
//!
11+
//! Provides a [`ColouredFormatter`] implementing tracing-subscriber's
12+
//! `FormatEvent` trait with HyperI's standard colour scheme:
13+
//!
14+
//! - **Timestamp:** dim
15+
//! - **Level:** ERROR=red bold, WARN=yellow, INFO=green, DEBUG=blue, TRACE=magenta dim
16+
//! - **Target:** cyan dim
17+
//! - **Source location:** dim
18+
//! - **Field names:** bold
19+
//! - **Message and values:** default
20+
21+
use std::fmt;
22+
23+
use owo_colors::{OwoColorize, Style};
24+
use tracing::Level;
25+
use tracing_subscriber::fmt::format::Writer;
26+
use tracing_subscriber::fmt::time::{FormatTime, UtcTime};
27+
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
28+
use tracing_subscriber::registry::LookupSpan;
29+
30+
/// Coloured log event formatter for terminal output.
31+
///
32+
/// When `enable_ansi` is false, outputs plain text without ANSI codes.
33+
#[derive(Debug, Clone)]
34+
#[allow(clippy::struct_excessive_bools)]
35+
pub struct ColouredFormatter {
36+
enable_ansi: bool,
37+
display_target: bool,
38+
display_file: bool,
39+
display_line_number: bool,
40+
}
41+
42+
impl ColouredFormatter {
43+
/// Create a new coloured formatter.
44+
#[must_use]
45+
pub fn new(enable_ansi: bool) -> Self {
46+
Self {
47+
enable_ansi,
48+
display_target: true,
49+
display_file: true,
50+
display_line_number: true,
51+
}
52+
}
53+
54+
/// Set whether to display source file name.
55+
#[must_use]
56+
pub fn with_file(mut self, display: bool) -> Self {
57+
self.display_file = display;
58+
self
59+
}
60+
61+
/// Set whether to display source line number.
62+
#[must_use]
63+
pub fn with_line_number(mut self, display: bool) -> Self {
64+
self.display_line_number = display;
65+
self
66+
}
67+
}
68+
69+
impl<S, N> FormatEvent<S, N> for ColouredFormatter
70+
where
71+
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
72+
N: for<'a> FormatFields<'a> + 'static,
73+
{
74+
fn format_event(
75+
&self,
76+
ctx: &FmtContext<'_, S, N>,
77+
mut writer: Writer<'_>,
78+
event: &tracing::Event<'_>,
79+
) -> fmt::Result {
80+
let meta = event.metadata();
81+
let ansi = self.enable_ansi && writer.has_ansi_escapes();
82+
83+
// Timestamp (dim)
84+
let timer = UtcTime::rfc_3339();
85+
let mut ts_buf = String::new();
86+
let _ = timer.format_time(&mut Writer::new(&mut ts_buf));
87+
if ansi {
88+
write!(writer, "{} ", ts_buf.style(dim_style()))?;
89+
} else {
90+
write!(writer, "{ts_buf} ")?;
91+
}
92+
93+
// Level (coloured)
94+
let level = meta.level();
95+
let level_str = format!("{level:>5}");
96+
if ansi {
97+
write!(writer, "{} ", level_str.style(level_style(*level)))?;
98+
} else {
99+
write!(writer, "{level_str} ")?;
100+
}
101+
102+
// Target (cyan dim)
103+
if self.display_target {
104+
let target = meta.target();
105+
if ansi {
106+
write!(writer, "{}:", target.style(target_style()))?;
107+
} else {
108+
write!(writer, "{target}:")?;
109+
}
110+
}
111+
112+
// Span context
113+
if let Some(scope) = ctx.event_scope() {
114+
for span in scope.from_root() {
115+
if ansi {
116+
write!(writer, "{}", span.name().style(span_style()))?;
117+
} else {
118+
write!(writer, "{}", span.name())?;
119+
}
120+
let ext = span.extensions();
121+
if let Some(fields) =
122+
ext.get::<tracing_subscriber::fmt::FormattedFields<N>>()
123+
{
124+
if !fields.is_empty() {
125+
write!(writer, "{{{fields}}}")?;
126+
}
127+
}
128+
write!(writer, ":")?;
129+
}
130+
}
131+
132+
// Space before message
133+
write!(writer, " ")?;
134+
135+
// Event fields (message + structured fields)
136+
ctx.format_fields(writer.by_ref(), event)?;
137+
138+
// Source location (dim)
139+
if self.display_file || self.display_line_number {
140+
let file = meta.file();
141+
let line = meta.line();
142+
match (self.display_file, self.display_line_number, file, line) {
143+
(true, true, Some(f), Some(l)) => {
144+
let loc = format!(" {f}:{l}");
145+
if ansi {
146+
write!(writer, "{}", loc.style(dim_style()))?;
147+
} else {
148+
write!(writer, "{loc}")?;
149+
}
150+
}
151+
(true, _, Some(f), _) => {
152+
let loc = format!(" {f}");
153+
if ansi {
154+
write!(writer, "{}", loc.style(dim_style()))?;
155+
} else {
156+
write!(writer, "{loc}")?;
157+
}
158+
}
159+
(_, true, _, Some(l)) => {
160+
let loc = format!(" :{l}");
161+
if ansi {
162+
write!(writer, "{}", loc.style(dim_style()))?;
163+
} else {
164+
write!(writer, "{loc}")?;
165+
}
166+
}
167+
_ => {}
168+
}
169+
}
170+
171+
writeln!(writer)
172+
}
173+
}
174+
175+
// ---------------------------------------------------------------------------
176+
// Style helpers
177+
// ---------------------------------------------------------------------------
178+
179+
fn level_style(level: Level) -> Style {
180+
match level {
181+
Level::ERROR => Style::new().red().bold(),
182+
Level::WARN => Style::new().yellow(),
183+
Level::INFO => Style::new().green(),
184+
Level::DEBUG => Style::new().blue(),
185+
Level::TRACE => Style::new().magenta().dimmed(),
186+
}
187+
}
188+
189+
fn dim_style() -> Style {
190+
Style::new().dimmed()
191+
}
192+
193+
fn target_style() -> Style {
194+
Style::new().cyan().dimmed()
195+
}
196+
197+
fn span_style() -> Style {
198+
Style::new().bold()
199+
}
200+
201+
#[cfg(test)]
202+
mod tests {
203+
use super::*;
204+
205+
#[test]
206+
fn test_level_style_returns_distinct_styles() {
207+
// Verify each level produces a style (no panics)
208+
let _ = level_style(Level::ERROR);
209+
let _ = level_style(Level::WARN);
210+
let _ = level_style(Level::INFO);
211+
let _ = level_style(Level::DEBUG);
212+
let _ = level_style(Level::TRACE);
213+
}
214+
215+
#[test]
216+
fn test_coloured_formatter_builder() {
217+
let fmt = ColouredFormatter::new(true)
218+
.with_file(false)
219+
.with_line_number(false);
220+
221+
assert!(fmt.enable_ansi);
222+
assert!(fmt.display_target);
223+
assert!(!fmt.display_file);
224+
assert!(!fmt.display_line_number);
225+
}
226+
}

0 commit comments

Comments
 (0)