Skip to content

Commit 4ddce69

Browse files
committed
feat(metrics): per-rung match attempt counter for hit miss ambiguous
1 parent a8dd55d commit 4ddce69

9 files changed

Lines changed: 402 additions & 121 deletions

File tree

service/src/matching/scoring.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,26 @@ pub enum Selection<'a, T> {
146146
None,
147147
}
148148

149+
/// Stable label for a rung outcome, used by the per-rung match metric.
150+
pub fn rung_outcome_label<T>(sel: &Selection<'_, T>) -> &'static str {
151+
match sel {
152+
Selection::Best(_) => "hit",
153+
Selection::Ambiguous => "ambiguous",
154+
Selection::None => "miss",
155+
}
156+
}
157+
158+
/// Records the outcome of a `pick_best` call as a per-rung metric and
159+
/// returns the `Selection` unchanged so call sites can continue to chain.
160+
pub fn record_pick_best<'a, T>(
161+
provider: &str,
162+
rung: &str,
163+
sel: Selection<'a, T>,
164+
) -> Selection<'a, T> {
165+
crate::metrics::record_match_rung(provider, rung, rung_outcome_label(&sel));
166+
sel
167+
}
168+
149169
/// Returns the unique max-scored candidate. Ties at the top of the score
150170
/// produce `Ambiguous`; empty input produces `None`.
151171
pub fn pick_best<'a, T>(
@@ -181,6 +201,17 @@ mod tests {
181201
use super::*;
182202
use crate::matching::name_parse::RegionTag;
183203

204+
#[test]
205+
fn rung_outcome_label_maps_each_variant() {
206+
let item = "x";
207+
let best: Selection<'_, &str> = Selection::Best(&item);
208+
assert_eq!(rung_outcome_label(&best), "hit");
209+
let ambig: Selection<'_, &str> = Selection::Ambiguous;
210+
assert_eq!(rung_outcome_label(&ambig), "ambiguous");
211+
let none: Selection<'_, &str> = Selection::None;
212+
assert_eq!(rung_outcome_label(&none), "miss");
213+
}
214+
184215
fn dat(year: Option<u16>) -> ParsedName {
185216
ParsedName {
186217
year,

service/src/metrics.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ static CACHE_L1_SIZE: OnceLock<IntGaugeVec> = OnceLock::new();
88
static IDENTIFY_ATTEMPTS: OnceLock<IntCounterVec> = OnceLock::new();
99
static SERVICE_ERRORS: OnceLock<IntCounterVec> = OnceLock::new();
1010
static METADATA_AUTO_MATCHES: OnceLock<IntCounterVec> = OnceLock::new();
11+
static MATCH_RUNG_OUTCOMES: OnceLock<IntCounterVec> = OnceLock::new();
1112
static METADATA_TOKEN_REFRESHES: OnceLock<IntCounterVec> = OnceLock::new();
1213
static BACKGROUND_JOB_RUNS: OnceLock<IntCounterVec> = OnceLock::new();
1314
static BACKGROUND_JOB_DURATION: OnceLock<HistogramVec> = OnceLock::new();
@@ -84,6 +85,18 @@ pub fn init(registry: &Registry) -> anyhow::Result<()> {
8485
.set(metadata_auto_matches)
8586
.map_err(|_| anyhow::anyhow!("metadata auto match metrics already initialised"))?;
8687

88+
let match_rung_outcomes = IntCounterVec::new(
89+
Opts::new(
90+
"api_match_rung_total",
91+
"Per-rung match attempt outcomes for each provider's matching ladder",
92+
),
93+
&["provider", "rung", "outcome"],
94+
)?;
95+
registry.register(Box::new(match_rung_outcomes.clone()))?;
96+
MATCH_RUNG_OUTCOMES
97+
.set(match_rung_outcomes)
98+
.map_err(|_| anyhow::anyhow!("match rung outcome metrics already initialised"))?;
99+
87100
let metadata_token_refreshes = IntCounterVec::new(
88101
Opts::new(
89102
"api_metadata_token_refresh_total",
@@ -300,6 +313,15 @@ pub fn record_metadata_auto_match(provider: &str, entity_type: &str, result: &st
300313
}
301314
}
302315

316+
/// Per-rung outcome counter. `outcome` is one of `hit`, `ambiguous`, `miss`.
317+
/// Together with `provider` and `rung` this lets dashboards compute
318+
/// hit-rate per rung per provider.
319+
pub fn record_match_rung(provider: &str, rung: &str, outcome: &str) {
320+
if let Some(counter) = MATCH_RUNG_OUTCOMES.get() {
321+
counter.with_label_values(&[provider, rung, outcome]).inc();
322+
}
323+
}
324+
303325
pub fn record_metadata_token_refresh(provider: &str, trigger: &str, result: &str) {
304326
if let Some(counter) = METADATA_TOKEN_REFRESHES.get() {
305327
counter

service/src/providers/emuready/matching/game.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::db::platform::{
77
};
88
use crate::matching::name_parse::parse_name;
99
use crate::matching::scoring::{
10-
CandidateGate, CandidateScore, Selection, gate_and_score, pick_best,
10+
CandidateGate, CandidateScore, Selection, gate_and_score, pick_best, record_pick_best,
1111
};
1212
use crate::matching::util::{clean_name, normalize_title};
1313
use crate::providers::MetadataProvider;
@@ -136,7 +136,11 @@ fn match_game_to_emuready(
136136
&db_conn,
137137
&mut redis_conn,
138138
&game,
139-
pick_best(direct.iter().map(|s| (s, s.score))),
139+
record_pick_best(
140+
"emuready",
141+
"direct",
142+
pick_best(direct.iter().map(|s| (s, s.score))),
143+
),
140144
AutomaticMatchReasonEnum::DirectName,
141145
"Direct Match",
142146
)
@@ -148,7 +152,11 @@ fn match_game_to_emuready(
148152
&db_conn,
149153
&mut redis_conn,
150154
&game,
151-
pick_best(normalized.iter().map(|s| (s, s.score))),
155+
record_pick_best(
156+
"emuready",
157+
"normalized",
158+
pick_best(normalized.iter().map(|s| (s, s.score))),
159+
),
152160
AutomaticMatchReasonEnum::NormalizedName,
153161
"Normalized Match",
154162
)
@@ -314,7 +322,11 @@ pub fn match_game_via_sibling_name_emuready(
314322
&mut redis_conn,
315323
&game,
316324
&sibling,
317-
pick_best(direct.iter().map(|s| (s, s.score))),
325+
record_pick_best(
326+
"emuready",
327+
"cross_direct",
328+
pick_best(direct.iter().map(|s| (s, s.score))),
329+
),
318330
AutomaticMatchReasonEnum::CrossProviderDirectName,
319331
"Direct",
320332
)
@@ -327,7 +339,11 @@ pub fn match_game_via_sibling_name_emuready(
327339
&mut redis_conn,
328340
&game,
329341
&sibling,
330-
pick_best(normalized.iter().map(|s| (s, s.score))),
342+
record_pick_best(
343+
"emuready",
344+
"cross_normalized",
345+
pick_best(normalized.iter().map(|s| (s, s.score))),
346+
),
331347
AutomaticMatchReasonEnum::CrossProviderNormalizedName,
332348
"Normalized",
333349
)

service/src/providers/igdb/matching/game.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::db::platform::{
77
};
88
use crate::matching::name_parse::parse_name;
99
use crate::matching::scoring::{
10-
CandidateGate, CandidateScore, Selection, gate_and_score, pick_best,
10+
CandidateGate, CandidateScore, Selection, gate_and_score, pick_best, record_pick_best,
1111
};
1212
use crate::matching::util::{clean_name, normalize_title};
1313
use crate::providers::MetadataProvider;
@@ -155,7 +155,11 @@ fn match_game_to_igdb(
155155
}
156156
}
157157

158-
match pick_best(direct.iter().map(|s| (s, s.score))) {
158+
match record_pick_best(
159+
"igdb",
160+
"direct",
161+
pick_best(direct.iter().map(|s| (s, s.score))),
162+
) {
159163
Selection::Best(s) => {
160164
return write_match(
161165
&db_conn,
@@ -174,7 +178,11 @@ fn match_game_to_igdb(
174178
Selection::None => {}
175179
}
176180

177-
match pick_best(normalized.iter().map(|s| (s, s.score))) {
181+
match record_pick_best(
182+
"igdb",
183+
"normalized",
184+
pick_best(normalized.iter().map(|s| (s, s.score))),
185+
) {
178186
Selection::Best(s) => {
179187
return write_match(
180188
&db_conn,
@@ -219,7 +227,11 @@ fn match_game_to_igdb(
219227
}
220228
}
221229

222-
match pick_best(alt_direct.iter().map(|s| (s, s.score))) {
230+
match record_pick_best(
231+
"igdb",
232+
"alternative",
233+
pick_best(alt_direct.iter().map(|s| (s, s.score))),
234+
) {
223235
Selection::Best(s) => {
224236
return write_match(
225237
&db_conn,
@@ -238,7 +250,11 @@ fn match_game_to_igdb(
238250
Selection::None => {}
239251
}
240252

241-
match pick_best(alt_normalized.iter().map(|s| (s, s.score))) {
253+
match record_pick_best(
254+
"igdb",
255+
"normalized_alternative",
256+
pick_best(alt_normalized.iter().map(|s| (s, s.score))),
257+
) {
242258
Selection::Best(s) => {
243259
return write_match(
244260
&db_conn,
@@ -448,7 +464,11 @@ pub fn match_game_via_sibling_name_igdb(
448464
&mut redis_conn,
449465
&game,
450466
&sibling,
451-
pick_best(direct.iter().map(|s| (s, s.score))),
467+
record_pick_best(
468+
"igdb",
469+
"cross_direct",
470+
pick_best(direct.iter().map(|s| (s, s.score))),
471+
),
452472
AutomaticMatchReasonEnum::CrossProviderDirectName,
453473
"Direct",
454474
)
@@ -462,7 +482,11 @@ pub fn match_game_via_sibling_name_igdb(
462482
&mut redis_conn,
463483
&game,
464484
&sibling,
465-
pick_best(normalized.iter().map(|s| (s, s.score))),
485+
record_pick_best(
486+
"igdb",
487+
"cross_normalized",
488+
pick_best(normalized.iter().map(|s| (s, s.score))),
489+
),
466490
AutomaticMatchReasonEnum::CrossProviderNormalizedName,
467491
"Normalized",
468492
)
@@ -502,7 +526,11 @@ pub fn match_game_via_sibling_name_igdb(
502526
&mut redis_conn,
503527
&game,
504528
&sibling,
505-
pick_best(alt_direct.iter().map(|s| (s, s.score))),
529+
record_pick_best(
530+
"igdb",
531+
"cross_alternative",
532+
pick_best(alt_direct.iter().map(|s| (s, s.score))),
533+
),
506534
AutomaticMatchReasonEnum::CrossProviderDirectName,
507535
"Alternative",
508536
)
@@ -516,7 +544,11 @@ pub fn match_game_via_sibling_name_igdb(
516544
&mut redis_conn,
517545
&game,
518546
&sibling,
519-
pick_best(alt_normalized.iter().map(|s| (s, s.score))),
547+
record_pick_best(
548+
"igdb",
549+
"cross_normalized_alternative",
550+
pick_best(alt_normalized.iter().map(|s| (s, s.score))),
551+
),
520552
AutomaticMatchReasonEnum::CrossProviderNormalizedName,
521553
"Normalized Alternative",
522554
)

service/src/providers/launchbox/matching/game.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::db::platform::{
1212
};
1313
use crate::matching::name_parse::{ParsedName, parse_name};
1414
use crate::matching::scoring::{
15-
CandidateGate, CandidateScore, Selection, gate_and_score, pick_best,
15+
CandidateGate, CandidateScore, Selection, gate_and_score, pick_best, record_pick_best,
1616
};
1717
use crate::matching::util::{clean_name, normalize_title};
1818
use crate::providers::MetadataProvider;
@@ -150,7 +150,11 @@ fn match_game_to_launchbox(
150150
&db_conn,
151151
&mut redis_conn,
152152
&game,
153-
pick_best(scored.iter().map(|s| (s, s.score))),
153+
record_pick_best(
154+
"launchbox",
155+
"direct",
156+
pick_best(scored.iter().map(|s| (s, s.score))),
157+
),
154158
AutomaticMatchReasonEnum::DirectName,
155159
"Direct Match",
156160
)
@@ -171,7 +175,11 @@ fn match_game_to_launchbox(
171175
&db_conn,
172176
&mut redis_conn,
173177
&game,
174-
pick_best(scored.iter().map(|s| (s, s.score))),
178+
record_pick_best(
179+
"launchbox",
180+
"alternative",
181+
pick_best(scored.iter().map(|s| (s, s.score))),
182+
),
175183
AutomaticMatchReasonEnum::AlternativeName,
176184
"Alternative Name",
177185
)
@@ -191,7 +199,11 @@ fn match_game_to_launchbox(
191199
&db_conn,
192200
&mut redis_conn,
193201
&game,
194-
pick_best(scored.iter().map(|s| (s, s.score))),
202+
record_pick_best(
203+
"launchbox",
204+
"normalized",
205+
pick_best(scored.iter().map(|s| (s, s.score))),
206+
),
195207
AutomaticMatchReasonEnum::NormalizedName,
196208
"Normalized Match",
197209
)
@@ -212,7 +224,11 @@ fn match_game_to_launchbox(
212224
&db_conn,
213225
&mut redis_conn,
214226
&game,
215-
pick_best(scored.iter().map(|s| (s, s.score))),
227+
record_pick_best(
228+
"launchbox",
229+
"normalized_alternative",
230+
pick_best(scored.iter().map(|s| (s, s.score))),
231+
),
216232
AutomaticMatchReasonEnum::NormalizedAlternativeName,
217233
"Normalized Alternative Name",
218234
)
@@ -365,7 +381,11 @@ pub fn match_game_via_sibling_name_launchbox(
365381
&mut redis_conn,
366382
&game,
367383
&sibling,
368-
pick_best(scored.iter().map(|s| (s, s.score))),
384+
record_pick_best(
385+
"launchbox",
386+
"cross_direct",
387+
pick_best(scored.iter().map(|s| (s, s.score))),
388+
),
369389
AutomaticMatchReasonEnum::CrossProviderDirectName,
370390
"Direct",
371391
)
@@ -387,7 +407,11 @@ pub fn match_game_via_sibling_name_launchbox(
387407
&mut redis_conn,
388408
&game,
389409
&sibling,
390-
pick_best(scored.iter().map(|s| (s, s.score))),
410+
record_pick_best(
411+
"launchbox",
412+
"cross_alternative",
413+
pick_best(scored.iter().map(|s| (s, s.score))),
414+
),
391415
AutomaticMatchReasonEnum::CrossProviderDirectName,
392416
"Direct alt",
393417
)
@@ -405,7 +429,11 @@ pub fn match_game_via_sibling_name_launchbox(
405429
&mut redis_conn,
406430
&game,
407431
&sibling,
408-
pick_best(scored.iter().map(|s| (s, s.score))),
432+
record_pick_best(
433+
"launchbox",
434+
"cross_normalized",
435+
pick_best(scored.iter().map(|s| (s, s.score))),
436+
),
409437
AutomaticMatchReasonEnum::CrossProviderNormalizedName,
410438
"Normalized",
411439
)
@@ -428,7 +456,11 @@ pub fn match_game_via_sibling_name_launchbox(
428456
&mut redis_conn,
429457
&game,
430458
&sibling,
431-
pick_best(scored.iter().map(|s| (s, s.score))),
459+
record_pick_best(
460+
"launchbox",
461+
"cross_normalized_alternative",
462+
pick_best(scored.iter().map(|s| (s, s.score))),
463+
),
432464
AutomaticMatchReasonEnum::CrossProviderNormalizedName,
433465
"Normalized alt",
434466
)

0 commit comments

Comments
 (0)