From ec5f566ba51b6475b79e7d5dd7f825bfdc6c87b2 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 2 Jan 2024 15:12:26 +0100 Subject: [PATCH] Fix Scheduled observation convention for lambdas Prior to this commit, the `DefaultScheduledTaskObservationConvention` would fail as it tried to add a `KeyValue` to the observation context that is `null`. This is rejected by the observation registry and should be prevented. This happened when registered scheduled methods were lambdas or part of anonymous classes. Those types do not have a canonical name and return `null` as a value there. This commit ensures that for these cases, the default convetion uses a `"ANONYMOUS"` value as the `"code.namespace"` keyvalue. Fixes gh-31918 --- .../ROOT/pages/integration/observability.adoc | 2 +- ...ultScheduledTaskObservationConvention.java | 9 +++++++-- ...heduledTaskObservationConventionTests.java | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/integration/observability.adoc b/framework-docs/modules/ROOT/pages/integration/observability.adoc index 13ca74ab118..e4208a28d8f 100644 --- a/framework-docs/modules/ROOT/pages/integration/observability.adoc +++ b/framework-docs/modules/ROOT/pages/integration/observability.adoc @@ -108,7 +108,7 @@ By default, the following `KeyValues` are created: |=== |Name | Description |`code.function` _(required)_|Name of Java `Method` that is scheduled for execution. -|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method. +|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method, or `"ANONYMOUS"` for anonymous classes. |`error` _(required)_|Class name of the exception thrown during the execution, or `"none"` if no exception happened. |`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution). diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java index 46c0385757f..4332c460b17 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,8 @@ public class DefaultScheduledTaskObservationConvention implements ScheduledTaskO private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN"); + private static final KeyValue CODE_NAMESPACE_ANONYMOUS = KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, "ANONYMOUS"); + @Override public String getName() { return DEFAULT_NAME; @@ -61,7 +63,10 @@ public class DefaultScheduledTaskObservationConvention implements ScheduledTaskO } protected KeyValue codeNamespace(ScheduledTaskObservationContext context) { - return KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, context.getTargetClass().getCanonicalName()); + if (context.getTargetClass().getCanonicalName() != null) { + return KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, context.getTargetClass().getCanonicalName()); + } + return CODE_NAMESPACE_ANONYMOUS; } protected KeyValue exception(ScheduledTaskObservationContext context) { diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java index 6f00d0a495f..408d0a33ab3 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConventionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,8 @@ class DefaultScheduledTaskObservationConventionTests { private final Method taskMethod = ClassUtils.getMethod(BeanWithScheduledMethods.class, "process"); + private final Method runMethod = ClassUtils.getMethod(Runnable.class, "run"); + private final ScheduledTaskObservationConvention convention = new DefaultScheduledTaskObservationConvention(); @@ -69,6 +71,21 @@ class DefaultScheduledTaskObservationConventionTests { assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("code.function", "process")); } + @Test + void observationShouldHaveTargetTypeForAnonymousClass() { + Runnable runnable = () -> { }; + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(runnable, runMethod); + assertThat(convention.getLowCardinalityKeyValues(context)) + .contains(KeyValue.of("code.namespace", "ANONYMOUS")); + } + + @Test + void observationShouldHaveMethodNameForAnonymousClass() { + Runnable runnable = () -> { }; + ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(runnable, runMethod); + assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("code.function", "run")); + } + @Test void observationShouldHaveSuccessfulOutcome() { ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod);