use crate::{
event::{AnomalyKind, EndReason, FlowStats},
extractor::L4Proto,
};
pub const METRIC_FLOWS_CREATED: &str = "flowscope_flows_created_total";
pub const METRIC_FLOWS_ENDED: &str = "flowscope_flows_ended_total";
pub const METRIC_FLOWS_ACTIVE: &str = "flowscope_flows_active";
pub const METRIC_PACKETS_UNMATCHED: &str = "flowscope_packets_unmatched_total";
pub const METRIC_PACKETS_DEDUPLICATED: &str = "flowscope_packets_deduplicated_total";
pub const METRIC_BYTES: &str = "flowscope_bytes_total";
pub const METRIC_FLOW_DURATION_SECONDS: &str = "flowscope_flow_duration_seconds";
pub const METRIC_FLOW_PACKETS: &str = "flowscope_flow_packets";
pub const METRIC_FLOW_BYTES: &str = "flowscope_flow_bytes";
pub const METRIC_ANOMALIES: &str = "flowscope_anomalies_total";
pub const METRIC_REASSEMBLY_DROPPED_OOO: &str = "flowscope_reassembly_dropped_ooo_total";
pub const METRIC_REASSEMBLY_BYTES_DROPPED_OVERSIZE: &str =
"flowscope_reassembly_bytes_dropped_oversize_total";
pub const METRIC_REASSEMBLER_HIGH_WATERMARK: &str = "flowscope_reassembler_high_watermark_bytes";
pub const METRIC_RETRANSMITS: &str = "flowscope_retransmits_total";
pub const METRIC_PARSER_CLOSED: &str = "flowscope_parser_closed_total";
pub const METRIC_PARSER_SIDE_STOPPED: &str = "flowscope_parser_side_stopped_total";
pub const METRIC_REASSEMBLY_GAP_BYTES: &str = "flowscope_reassembly_gap_bytes_total";
pub const METRIC_FLOW_TICKS: &str = "flowscope_flow_ticks_total";
pub const METRIC_HTTP_MESSAGES: &str = "flowscope_http_messages_total";
pub const METRIC_HTTP_POISONED: &str = "flowscope_http_poisoned_total";
pub(crate) fn l4_label(l4: Option<L4Proto>) -> &'static str {
match l4 {
Some(L4Proto::Tcp) => "tcp",
Some(L4Proto::Udp) => "udp",
_ => "other",
}
}
pub(crate) fn reason_label(reason: EndReason) -> &'static str {
match reason {
EndReason::Fin => "fin",
EndReason::Rst => "rst",
EndReason::IdleTimeout => "idle",
EndReason::Evicted => "evicted",
EndReason::BufferOverflow => "buffer_overflow",
EndReason::ParseError => "parse_error",
EndReason::ParserDone => "parser_done",
EndReason::ForceClosed => "force_closed",
EndReason::StreamGap => "stream_gap",
}
}
pub(crate) fn anomaly_label(kind: &AnomalyKind) -> &'static str {
match kind {
AnomalyKind::BufferOverflow { .. } => "buffer_overflow",
AnomalyKind::OutOfOrderSegment { .. } => "ooo_segment",
AnomalyKind::FlowTableEvictionPressure { .. } => "flow_table_eviction",
AnomalyKind::SessionParseError { .. } => "parse_error",
AnomalyKind::RetransmittedSegment { .. } => "retransmit",
AnomalyKind::ReassemblerHighWatermark { .. } => "reassembler_high_watermark",
AnomalyKind::TcpRexmitInconsistency { .. } => "tcp_rexmit_inconsistency",
AnomalyKind::GlobalMemcapHit { .. } => "global_memcap_hit",
AnomalyKind::StreamGap { .. } => "stream_gap",
AnomalyKind::OutOfWindowSegment { .. } => "out_of_window_segment",
}
}
#[cfg(feature = "metrics")]
pub(crate) fn record_flow_created(l4: Option<L4Proto>) {
metrics::counter!(METRIC_FLOWS_CREATED, "l4" => l4_label(l4)).increment(1);
metrics::gauge!(METRIC_FLOWS_ACTIVE).increment(1.0);
}
#[cfg(feature = "metrics")]
pub(crate) fn record_flow_ended(reason: EndReason, stats: &FlowStats) {
metrics::counter!(METRIC_FLOWS_ENDED, "reason" => reason_label(reason)).increment(1);
metrics::gauge!(METRIC_FLOWS_ACTIVE).decrement(1.0);
metrics::counter!(METRIC_BYTES, "side" => "initiator").increment(stats.bytes_initiator);
metrics::counter!(METRIC_BYTES, "side" => "responder").increment(stats.bytes_responder);
let duration = duration_seconds(stats);
metrics::histogram!(METRIC_FLOW_DURATION_SECONDS).record(duration);
metrics::histogram!(METRIC_FLOW_PACKETS)
.record((stats.packets_initiator + stats.packets_responder) as f64);
metrics::histogram!(METRIC_FLOW_BYTES)
.record((stats.bytes_initiator + stats.bytes_responder) as f64);
}
#[cfg(all(feature = "metrics", feature = "reassembler"))]
pub(crate) fn record_reassembly_diagnostics(stats: &FlowStats) {
if stats.reassembly_dropped_ooo_initiator > 0 {
metrics::counter!(METRIC_REASSEMBLY_DROPPED_OOO, "side" => "initiator")
.increment(stats.reassembly_dropped_ooo_initiator);
}
if stats.reassembly_dropped_ooo_responder > 0 {
metrics::counter!(METRIC_REASSEMBLY_DROPPED_OOO, "side" => "responder")
.increment(stats.reassembly_dropped_ooo_responder);
}
if stats.reassembly_bytes_dropped_oversize_initiator > 0 {
metrics::counter!(METRIC_REASSEMBLY_BYTES_DROPPED_OVERSIZE, "side" => "initiator")
.increment(stats.reassembly_bytes_dropped_oversize_initiator);
}
if stats.reassembly_bytes_dropped_oversize_responder > 0 {
metrics::counter!(METRIC_REASSEMBLY_BYTES_DROPPED_OVERSIZE, "side" => "responder")
.increment(stats.reassembly_bytes_dropped_oversize_responder);
}
if stats.reassembler_high_watermark_initiator > 0 {
metrics::histogram!(METRIC_REASSEMBLER_HIGH_WATERMARK, "side" => "initiator")
.record(stats.reassembler_high_watermark_initiator as f64);
}
if stats.reassembler_high_watermark_responder > 0 {
metrics::histogram!(METRIC_REASSEMBLER_HIGH_WATERMARK, "side" => "responder")
.record(stats.reassembler_high_watermark_responder as f64);
}
if stats.retransmits_initiator > 0 {
metrics::counter!(METRIC_RETRANSMITS, "side" => "initiator")
.increment(stats.retransmits_initiator);
}
if stats.retransmits_responder > 0 {
metrics::counter!(METRIC_RETRANSMITS, "side" => "responder")
.increment(stats.retransmits_responder);
}
if stats.reassembly_gap_bytes_initiator > 0 {
metrics::counter!(METRIC_REASSEMBLY_GAP_BYTES, "side" => "initiator")
.increment(stats.reassembly_gap_bytes_initiator);
}
if stats.reassembly_gap_bytes_responder > 0 {
metrics::counter!(METRIC_REASSEMBLY_GAP_BYTES, "side" => "responder")
.increment(stats.reassembly_gap_bytes_responder);
}
}
#[cfg(all(not(feature = "metrics"), feature = "reassembler"))]
#[inline(always)]
pub(crate) fn record_reassembly_diagnostics(_stats: &FlowStats) {}
#[cfg(all(feature = "metrics", feature = "reassembler"))]
pub(crate) fn record_flow_tick(_stats: &FlowStats) {
metrics::counter!(METRIC_FLOW_TICKS).increment(1);
}
#[cfg(all(not(feature = "metrics"), feature = "reassembler"))]
#[inline(always)]
pub(crate) fn record_flow_tick(_stats: &FlowStats) {}
#[cfg(feature = "metrics")]
pub(crate) fn record_packet_unmatched() {
metrics::counter!(METRIC_PACKETS_UNMATCHED).increment(1);
}
#[cfg(feature = "metrics")]
pub(crate) fn record_packet_deduplicated() {
metrics::counter!(METRIC_PACKETS_DEDUPLICATED).increment(1);
}
#[cfg(all(
feature = "metrics",
feature = "session",
feature = "extractors",
feature = "reassembler"
))]
pub(crate) fn record_parser_closed(kind: crate::ParserKind, reason: EndReason) {
metrics::counter!(
METRIC_PARSER_CLOSED,
"parser_kind" => kind.as_str(),
"reason" => reason_label(reason)
)
.increment(1);
}
#[cfg(all(
not(feature = "metrics"),
feature = "session",
feature = "extractors",
feature = "reassembler"
))]
#[inline(always)]
pub(crate) fn record_parser_closed(_kind: crate::ParserKind, _reason: EndReason) {}
#[cfg(all(
feature = "metrics",
feature = "session",
feature = "extractors",
feature = "reassembler"
))]
pub(crate) fn record_parser_side_stopped(
kind: crate::ParserKind,
side: crate::FlowSide,
reason: EndReason,
) {
metrics::counter!(
METRIC_PARSER_SIDE_STOPPED,
"parser_kind" => kind.as_str(),
"side" => side.as_str(),
"reason" => reason_label(reason)
)
.increment(1);
}
#[cfg(all(
not(feature = "metrics"),
feature = "session",
feature = "extractors",
feature = "reassembler"
))]
#[inline(always)]
pub(crate) fn record_parser_side_stopped(
_kind: crate::ParserKind,
_side: crate::FlowSide,
_reason: EndReason,
) {
}
#[cfg(all(feature = "metrics", feature = "reassembler"))]
pub(crate) fn record_anomaly(kind: &AnomalyKind) {
metrics::counter!(METRIC_ANOMALIES, "kind" => anomaly_label(kind)).increment(1);
}
#[cfg(feature = "metrics")]
fn duration_seconds(stats: &FlowStats) -> f64 {
let start = stats.started.to_duration();
let end = stats.last_seen.to_duration();
end.saturating_sub(start).as_secs_f64()
}
#[cfg(not(feature = "metrics"))]
#[inline(always)]
pub(crate) fn record_flow_created(_l4: Option<L4Proto>) {}
#[cfg(not(feature = "metrics"))]
#[inline(always)]
pub(crate) fn record_flow_ended(_reason: EndReason, _stats: &FlowStats) {}
#[cfg(not(feature = "metrics"))]
#[inline(always)]
pub(crate) fn record_packet_unmatched() {}
#[cfg(not(feature = "metrics"))]
#[inline(always)]
pub(crate) fn record_packet_deduplicated() {}
#[cfg(all(not(feature = "metrics"), feature = "reassembler"))]
#[inline(always)]
pub(crate) fn record_anomaly(_kind: &AnomalyKind) {}
#[cfg(feature = "tracing")]
pub(crate) fn trace_flow_started(l4: Option<L4Proto>) {
tracing::info!(target: "flowscope.flow", ?l4, "flow started");
}
#[cfg(feature = "tracing")]
pub(crate) fn trace_flow_ended(reason: EndReason, stats: &FlowStats) {
tracing::info!(
target: "flowscope.flow",
?reason,
packets = stats.packets_initiator + stats.packets_responder,
bytes = stats.bytes_initiator + stats.bytes_responder,
"flow ended"
);
}
#[cfg(all(feature = "tracing", feature = "reassembler"))]
pub(crate) fn trace_anomaly(kind: &AnomalyKind) {
tracing::warn!(
target: "flowscope.anomaly",
severity = %kind.severity(),
?kind,
"anomaly"
);
}
#[cfg(not(feature = "tracing"))]
#[inline(always)]
pub(crate) fn trace_flow_started(_l4: Option<L4Proto>) {}
#[cfg(not(feature = "tracing"))]
#[inline(always)]
pub(crate) fn trace_flow_ended(_reason: EndReason, _stats: &FlowStats) {}
#[cfg(all(not(feature = "tracing"), feature = "reassembler"))]
#[inline(always)]
pub(crate) fn trace_anomaly(_kind: &AnomalyKind) {}
#[cfg(all(
feature = "tracing",
feature = "extractors",
feature = "reassembler",
feature = "session"
))]
pub(crate) fn trace_session_message<M: std::fmt::Debug>(side: crate::event::FlowSide, msg: &M) {
tracing::trace!(
target: "flowscope.message",
?side,
message = ?msg,
"session message"
);
}
#[cfg(all(
feature = "extractors",
feature = "reassembler",
feature = "session",
not(feature = "tracing")
))]
#[inline(always)]
pub(crate) fn trace_session_message<M>(_side: crate::event::FlowSide, _msg: &M) {}
#[cfg(all(feature = "metrics", feature = "http"))]
pub(crate) fn record_http_message(is_request: bool) {
let direction = if is_request { "request" } else { "response" };
metrics::counter!(METRIC_HTTP_MESSAGES, "direction" => direction).increment(1);
}
#[cfg(all(not(feature = "metrics"), feature = "http"))]
pub(crate) fn record_http_message(_is_request: bool) {}
#[cfg(all(feature = "metrics", feature = "http"))]
pub(crate) fn record_http_poisoned(reason: crate::http::HttpPoison) {
metrics::counter!(METRIC_HTTP_POISONED, "reason" => reason.as_str()).increment(1);
}
#[cfg(all(not(feature = "metrics"), feature = "http"))]
pub(crate) fn record_http_poisoned(_reason: crate::http::HttpPoison) {}