2525import org .junit .jupiter .api .Test ;
2626import org .mockito .Mockito ;
2727
28+ import com .vaadin .flow .component .Component ;
29+ import com .vaadin .flow .component .UI ;
30+ import com .vaadin .flow .dom .Element ;
31+ import com .vaadin .flow .dom .ElementFactory ;
2832import com .vaadin .flow .server .communication .RpcInvocationEvent ;
2933import com .vaadin .observability .micrometer .trace .ObservationNames ;
3034
@@ -35,6 +39,7 @@ private static final class RecordingHandler
3539
3640 final List <String > names = new ArrayList <>();
3741 final List <Map <String , String >> tags = new ArrayList <>();
42+ final List <Map <String , String >> highCardinalityTags = new ArrayList <>();
3843 final AtomicBoolean errored = new AtomicBoolean ();
3944 Observation .Context lastContext ;
4045
@@ -47,6 +52,11 @@ public void onStop(Observation.Context ctx) {
4752 snap .put (kv .getKey (), kv .getValue ());
4853 }
4954 tags .add (snap );
55+ Map <String , String > highSnap = new HashMap <>();
56+ for (KeyValue kv : ctx .getHighCardinalityKeyValues ()) {
57+ highSnap .put (kv .getKey (), kv .getValue ());
58+ }
59+ highCardinalityTags .add (highSnap );
5060 if (ctx .getError () != null ) {
5161 errored .set (true );
5262 }
@@ -234,4 +244,139 @@ void errorStateDoesNotBleedIntoSubsequentInvocation() {
234244 Assertions .assertEquals (1L , successTimer .count (),
235245 "error state must not bleed into next invocation" );
236246 }
247+
248+ @ Test
249+ void observationPathAddsEventNameAsHighCardinalitySpanAttribute () {
250+ SimpleMeterRegistry simpleRegistry = new SimpleMeterRegistry ();
251+ ObservationRegistry observationRegistry = ObservationRegistry .create ();
252+ RecordingHandler recorder = new RecordingHandler ();
253+ observationRegistry .observationConfig ()
254+ .observationHandler (
255+ new DefaultMeterObservationHandler (simpleRegistry ))
256+ .observationHandler (recorder );
257+
258+ RpcMetricsBinder binder = new RpcMetricsBinder (simpleRegistry ,
259+ observationRegistry ,
260+ ObservabilitySettings .builder ().traces (true ).build ());
261+
262+ RpcInvocationEvent event = Mockito .mock (RpcInvocationEvent .class );
263+ Mockito .when (event .getType ()).thenReturn ("event" );
264+ Mockito .when (event .getName ()).thenReturn ("click" );
265+
266+ binder .invocationStarted (event );
267+ binder .invocationEnded (event );
268+
269+ Assertions .assertEquals ("click" ,
270+ recorder .highCardinalityTags .get (0 )
271+ .get (ObservationNames .KEY_EVENT_NAME ),
272+ "span should carry the invocation name as a high-cardinality attribute" );
273+
274+ // The event name must not leak into the Timer tags.
275+ Assertions .assertFalse (
276+ recorder .tags .get (0 )
277+ .containsKey (ObservationNames .KEY_EVENT_NAME ),
278+ "event name must not be a low-cardinality Timer tag" );
279+ Timer timer = simpleRegistry .find (MeterNames .RPC_DURATION )
280+ .tag (MeterNames .TAG_TYPE , "event" )
281+ .tag (MeterNames .TAG_OUTCOME , MeterNames .OUTCOME_SUCCESS )
282+ .timer ();
283+ Assertions .assertNotNull (timer ,
284+ "Timer tags should remain type + outcome only" );
285+ Assertions .assertEquals (1L , timer .count ());
286+ }
287+
288+ @ Test
289+ void observationPathOmitsEventNameWhenNull () {
290+ SimpleMeterRegistry simpleRegistry = new SimpleMeterRegistry ();
291+ ObservationRegistry observationRegistry = ObservationRegistry .create ();
292+ RecordingHandler recorder = new RecordingHandler ();
293+ observationRegistry .observationConfig ()
294+ .observationHandler (
295+ new DefaultMeterObservationHandler (simpleRegistry ))
296+ .observationHandler (recorder );
297+
298+ RpcMetricsBinder binder = new RpcMetricsBinder (simpleRegistry ,
299+ observationRegistry ,
300+ ObservabilitySettings .builder ().traces (true ).build ());
301+
302+ RpcInvocationEvent event = Mockito .mock (RpcInvocationEvent .class );
303+ Mockito .when (event .getType ()).thenReturn ("event" );
304+ Mockito .when (event .getName ()).thenReturn (null );
305+
306+ binder .invocationStarted (event );
307+ binder .invocationEnded (event );
308+
309+ Assertions .assertFalse (
310+ recorder .highCardinalityTags .get (0 )
311+ .containsKey (ObservationNames .KEY_EVENT_NAME ),
312+ "no event name attribute should be added when getName() is null" );
313+ }
314+
315+ @ Test
316+ void observationPathAddsComponentClassWhenNodeResolves () {
317+ SimpleMeterRegistry simpleRegistry = new SimpleMeterRegistry ();
318+ ObservationRegistry observationRegistry = ObservationRegistry .create ();
319+ RecordingHandler recorder = new RecordingHandler ();
320+ observationRegistry .observationConfig ()
321+ .observationHandler (
322+ new DefaultMeterObservationHandler (simpleRegistry ))
323+ .observationHandler (recorder );
324+
325+ RpcMetricsBinder binder = new RpcMetricsBinder (simpleRegistry ,
326+ observationRegistry ,
327+ ObservabilitySettings .builder ().traces (true ).build ());
328+
329+ // Build a real UI with an attached component so the binder can resolve
330+ // the component class from the target node id.
331+ UI ui = new UI ();
332+ Element element = ElementFactory .createDiv ();
333+ Component component = new Component (element ) {
334+ };
335+ ui .getElement ().appendChild (element );
336+ int nodeId = element .getNode ().getId ();
337+
338+ RpcInvocationEvent event = Mockito .mock (RpcInvocationEvent .class );
339+ Mockito .when (event .getType ()).thenReturn ("event" );
340+ Mockito .when (event .getUI ()).thenReturn (ui );
341+ Mockito .when (event .getNodeId ()).thenReturn (nodeId );
342+
343+ binder .invocationStarted (event );
344+ binder .invocationEnded (event );
345+
346+ Assertions .assertEquals (component .getClass ().getName (),
347+ recorder .highCardinalityTags .get (0 )
348+ .get (ObservationNames .KEY_COMPONENT ),
349+ "span should carry the targeted component class as a high-cardinality attribute" );
350+ Assertions .assertFalse (
351+ recorder .tags .get (0 )
352+ .containsKey (ObservationNames .KEY_COMPONENT ),
353+ "component class must not be a low-cardinality Timer tag" );
354+ }
355+
356+ @ Test
357+ void observationPathOmitsComponentWhenNodeIdNegative () {
358+ SimpleMeterRegistry simpleRegistry = new SimpleMeterRegistry ();
359+ ObservationRegistry observationRegistry = ObservationRegistry .create ();
360+ RecordingHandler recorder = new RecordingHandler ();
361+ observationRegistry .observationConfig ()
362+ .observationHandler (
363+ new DefaultMeterObservationHandler (simpleRegistry ))
364+ .observationHandler (recorder );
365+
366+ RpcMetricsBinder binder = new RpcMetricsBinder (simpleRegistry ,
367+ observationRegistry ,
368+ ObservabilitySettings .builder ().traces (true ).build ());
369+
370+ RpcInvocationEvent event = Mockito .mock (RpcInvocationEvent .class );
371+ Mockito .when (event .getType ()).thenReturn ("event" );
372+ Mockito .when (event .getNodeId ()).thenReturn (-1 );
373+
374+ binder .invocationStarted (event );
375+ binder .invocationEnded (event );
376+
377+ Assertions .assertFalse (
378+ recorder .highCardinalityTags .get (0 )
379+ .containsKey (ObservationNames .KEY_COMPONENT ),
380+ "no component attribute should be added when the invocation targets no node" );
381+ }
237382}
0 commit comments