@@ -35,8 +35,12 @@ pub struct WafMetrics {
3535 // Ruleset version (context for tag generation)
3636 rules_version : Option < String > ,
3737
38- /// Whether a non-RASP evaluation hit an error
39- waf_hit_error : bool ,
38+ /// The error code of the last non-RASP evaluation that hit an error, if any
39+ waf_error_code : Option < i32 > ,
40+
41+ /// The RASP evaluation that hit an error, if any. RASP errors are reported
42+ /// separately from non-RASP ones (appsec.rasp.error vs appsec.waf.error)
43+ rasp_error : Option < RaspError > ,
4044
4145 /// Total WAF execution time in milliseconds (non-RASP calls only)
4246 waf_duration : Duration ,
@@ -74,6 +78,14 @@ pub struct WafMetrics {
7478 rate_limited : bool ,
7579}
7680
81+ #[ derive( Debug ) ]
82+ struct RaspError {
83+ /// The numeric error returned by ddwaf_run, or -127 if from the bindings.
84+ code : i32 ,
85+ rule_type : String ,
86+ rule_variant : String ,
87+ }
88+
7789#[ derive( Default , Debug , Clone ) ]
7890pub struct RaspRuleMetrics {
7991 /// Total number of RASP rule evaluations, whether they matched or not
@@ -95,7 +107,8 @@ impl WafMetrics {
95107 pub fn new ( rules_version : Option < String > ) -> Self {
96108 Self {
97109 rules_version,
98- waf_hit_error : false ,
110+ waf_error_code : None ,
111+ rasp_error : None ,
99112 waf_duration : Duration :: ZERO ,
100113 waf_hit_timeout : false ,
101114 rasp_duration : Duration :: ZERO ,
@@ -117,8 +130,22 @@ impl WafMetrics {
117130 self . rate_limited = rate_limited;
118131 }
119132
120- pub fn record_non_rasp_error_eval ( & mut self ) {
121- self . waf_hit_error = true ;
133+ pub fn record_non_rasp_error_eval ( & mut self , error_code : i32 ) {
134+ self . waf_error_code = Some ( error_code) ;
135+ }
136+
137+ pub fn record_rasp_error_eval ( & mut self , error_code : i32 , rule_type : & str , rule_variant : & str ) {
138+ self . rasp_error = Some ( RaspError {
139+ code : error_code,
140+ rule_type : rule_type. to_string ( ) ,
141+ rule_variant : rule_variant. to_string ( ) ,
142+ } ) ;
143+ // questionable but we still count towards these metrics even with error
144+ self . rasp_rule_evals += 1 ;
145+ self . rasp_per_rule
146+ . entry ( ( rule_type. to_string ( ) , rule_variant. to_string ( ) ) )
147+ . or_default ( )
148+ . evals += 1 ;
122149 }
123150
124151 pub fn record_non_rasp_eval ( & mut self , run_output : & libddwaf:: RunOutput ) {
@@ -193,68 +220,63 @@ impl telemetry::TelemetryMetricsGenerator for WafMetrics {
193220 & ' _ self ,
194221 submitter : & mut dyn telemetry:: TelemetryMetricSubmitter ,
195222 ) {
223+ let base_tags = {
224+ let mut tags = telemetry:: TelemetryTags :: new ( ) ;
225+ tags. add ( "waf_version" , crate :: service:: Service :: waf_version ( ) ) ;
226+ tags. add (
227+ "event_rules_version" ,
228+ self . rules_version . as_deref ( ) . unwrap_or ( "unknown" ) ,
229+ ) ;
230+ tags
231+ } ;
232+
196233 // waf.requests metrics
197234 // RFC-1012: all boolean tags must be emitted regardless of value.
198- let mut tags = telemetry:: TelemetryTags :: new ( ) ;
199- tags. add ( "waf_version" , crate :: service:: Service :: waf_version ( ) ) ;
200- tags. add (
201- "event_rules_version" ,
202- self . rules_version . as_deref ( ) . unwrap_or ( "unknown" ) ,
203- ) ;
235+ let mut tags = base_tags. clone ( ) ;
204236 tags. add ( "rule_triggered" , bool_tag ( self . had_triggers ) ) ;
205237 // block_failure is not tracked: the PHP layer is assumed to always succeed at blocking.
206238 // Therefore request_blocked == "WAF requested a block" == "block succeeded".
207239 // request_excluded is not tracked: libddwaf applies exclusion filters internally and
208240 // does not expose whether a request was excluded in RunOutput.
209241 tags. add ( "request_blocked" , bool_tag ( self . request_blocked ) ) ;
210- tags. add ( "waf_error" , bool_tag ( self . waf_hit_error ) ) ;
242+ tags. add ( "waf_error" , bool_tag ( self . waf_error_code . is_some ( ) ) ) ;
211243 tags. add ( "waf_timeout" , bool_tag ( self . waf_hit_timeout ) ) ;
212244 tags. add ( "input_truncated" , bool_tag ( self . input_truncated ) ) ;
213245 tags. add ( "rate_limited" , bool_tag ( self . rate_limited ) ) ;
214246 submitter. submit_metric ( telemetry:: WAF_REQUESTS , 1.0 , tags) ;
215247
248+ // waf.error
249+ if let Some ( error_code) = self . waf_error_code {
250+ let mut err_tags = base_tags. clone ( ) ;
251+ err_tags. add ( "waf_error" , error_code. to_string ( ) ) ;
252+ submitter. submit_metric ( telemetry:: WAF_ERROR , 1.0 , err_tags) ;
253+ }
254+
216255 // waf.duration distribution: one observation per request, value in microseconds
217256 if !self . waf_duration . is_zero ( ) {
218- let mut dur_tags = telemetry:: TelemetryTags :: new ( ) ;
219- dur_tags. add ( "waf_version" , crate :: service:: Service :: waf_version ( ) ) ;
220- dur_tags. add (
221- "event_rules_version" ,
222- self . rules_version . as_deref ( ) . unwrap_or ( "unknown" ) ,
223- ) ;
224257 submitter. submit_metric (
225258 telemetry:: WAF_DURATION_DIST ,
226259 self . waf_duration . as_micros ( ) as f64 ,
227- dur_tags ,
260+ base_tags . clone ( ) ,
228261 ) ;
229262 }
230263
231264 // rasp.duration distribution: cumulative internal libddwaf runtime per request, in microseconds
232265 if !self . rasp_duration . is_zero ( ) {
233- let mut dur_tags = telemetry:: TelemetryTags :: new ( ) ;
234- dur_tags. add ( "waf_version" , crate :: service:: Service :: waf_version ( ) ) ;
235- dur_tags. add (
236- "event_rules_version" ,
237- self . rules_version . as_deref ( ) . unwrap_or ( "unknown" ) ,
238- ) ;
239266 submitter. submit_metric (
240267 telemetry:: RASP_DURATION_DIST ,
241268 self . rasp_duration . as_micros ( ) as f64 ,
242- dur_tags ,
269+ base_tags . clone ( ) ,
243270 ) ;
244271 }
245272
246273 // Rasp rule metrics
247274 for ( ( rule_type, rule_variant) , metrics) in & self . rasp_per_rule {
248- let mut tags = telemetry :: TelemetryTags :: new ( ) ;
275+ let mut tags = base_tags . clone ( ) ;
249276 tags. add ( "rule_type" , rule_type) ;
250277 if !rule_variant. is_empty ( ) {
251278 tags. add ( "rule_variant" , rule_variant) ;
252279 }
253- tags. add ( "waf_version" , crate :: service:: Service :: waf_version ( ) ) ;
254- tags. add (
255- "event_rules_version" ,
256- self . rules_version . as_deref ( ) . unwrap_or ( "unknown" ) ,
257- ) ;
258280
259281 if metrics. evals > 0 {
260282 submitter. submit_metric (
@@ -287,6 +309,17 @@ impl telemetry::TelemetryMetricsGenerator for WafMetrics {
287309 // tests expect this to always be sent, even if 0
288310 submitter. submit_metric ( telemetry:: RASP_TIMEOUT , metrics. timeouts as f64 , tags) ;
289311 }
312+
313+ // rasp.error
314+ if let Some ( ref err) = self . rasp_error {
315+ let mut err_tags = base_tags. clone ( ) ;
316+ err_tags. add ( "rule_type" , & err. rule_type ) ;
317+ if !err. rule_variant . is_empty ( ) {
318+ err_tags. add ( "rule_variant" , & err. rule_variant ) ;
319+ }
320+ err_tags. add ( "waf_error" , err. code . to_string ( ) ) ;
321+ submitter. submit_metric ( telemetry:: RASP_ERROR , 1.0 , err_tags) ;
322+ }
290323 }
291324}
292325
@@ -320,12 +353,3 @@ fn bool_tag(value: bool) -> &'static str {
320353 "false"
321354 }
322355}
323-
324- trait DurationExt {
325- fn duration_ms_f64 ( & self ) -> f64 ;
326- }
327- impl DurationExt for Duration {
328- fn duration_ms_f64 ( & self ) -> f64 {
329- self . as_secs ( ) as f64 * 1_000.0 + self . subsec_nanos ( ) as f64 / 1_000_000.0
330- }
331- }
0 commit comments