diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index f06176bb95..e4ad805552 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -79,6 +79,7 @@ public class MetricsProperties { return this.enable; } + @DeprecatedConfigurationProperty(replacement = "management.observations.key-values") public Map getTags() { return this.tags; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java index 216bc42624..440fc9bf96 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -43,6 +43,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClas import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; /** * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Observation API. @@ -75,6 +76,12 @@ public class ObservationAutoConfiguration { return ObservationRegistry.create(); } + @Bean + @Order(0) + PropertiesObservationFilter propertiesObservationFilter(ObservationProperties properties) { + return new PropertiesObservationFilter(properties); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnMissingClass("io.micrometer.tracing.Tracer") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index e4668d7f4b..92f8e8b0fe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.observation; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -30,10 +33,23 @@ public class ObservationProperties { private final Http http = new Http(); + /** + * Common key-values that are applied to every observation. + */ + private Map keyValues = new LinkedHashMap<>(); + public Http getHttp() { return this.http; } + public Map getKeyValues() { + return this.keyValues; + } + + public void setKeyValues(Map keyValues) { + this.keyValues = keyValues; + } + public static class Http { private final Client client = new Client(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java new file mode 100644 index 0000000000..d1116aa4dd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.observation; + +import java.util.Map.Entry; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationFilter; + +/** + * {@link ObservationFilter} to apply settings from {@link ObservationProperties}. + * + * @author Moritz Halbritter + */ +class PropertiesObservationFilter implements ObservationFilter { + + private final ObservationFilter delegate; + + PropertiesObservationFilter(ObservationProperties properties) { + this.delegate = createDelegate(properties); + } + + @Override + public Context map(Context context) { + return this.delegate.map(context); + } + + private static ObservationFilter createDelegate(ObservationProperties properties) { + if (properties.getKeyValues().isEmpty()) { + return (context) -> context; + } + KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue); + return (context) -> context.addLowCardinalityKeyValues(keyValues); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index 28e4bd0100..186adde605 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -189,6 +189,21 @@ class ObservationAutoConfigurationTests { }); } + @Test + void shouldSupplyPropertiesObservationFilterBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilter.class)); + } + + @Test + void shouldApplyCommonKeyValuesToObservations() { + this.contextRunner.withPropertyValues("management.observations.key-values.a=alpha").run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("keyvalues", observationRegistry).stop(); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.get("keyvalues").tag("a", "alpha").timer().count()).isOne(); + }); + } + @Test void autoConfiguresGlobalObservationConventions() { this.contextRunner.withUserConfiguration(CustomGlobalObservationConvention.class).run((context) -> { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java new file mode 100644 index 0000000000..b352f9695d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation.Context; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesObservationFilter}. + * + * @author Moritz Halbritter + */ +class PropertiesObservationFilterTests { + + @Test + void shouldDoNothingIfKeyValuesAreEmpty() { + PropertiesObservationFilter filter = createFilter(); + Context mapped = mapContext(filter, "a", "alpha"); + assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha")); + } + + @Test + void shouldAddKeyValues() { + PropertiesObservationFilter filter = createFilter("b", "beta"); + Context mapped = mapContext(filter, "a", "alpha"); + assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"), + KeyValue.of("b", "beta")); + } + + private static Context mapContext(PropertiesObservationFilter filter, String... initialKeyValues) { + Context context = new Context(); + context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues)); + return filter.map(context); + } + + private static PropertiesObservationFilter createFilter(String... keyValues) { + ObservationProperties properties = new ObservationProperties(); + for (int i = 0; i < keyValues.length; i += 2) { + properties.getKeyValues().put(keyValues[i], keyValues[i + 1]); + } + return new PropertiesObservationFilter(properties); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 2f9ce53ec6..96d671570b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -1100,19 +1100,8 @@ These use the global registry that is not Spring-managed. [[actuator.metrics.customizing.common-tags]] ==== Common Tags -Common tags are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. -Commons tags are applied to all meters and can be configured, as the following example shows: -[source,yaml,indent=0,subs="verbatim",configprops,configblocks] ----- - management: - metrics: - tags: - region: "us-east-1" - stack: "prod" ----- - -The preceding example adds `region` and `stack` tags to all meters with a value of `us-east-1` and `prod`, respectively. +You can configure common tags using the <>. NOTE: The order of common tags is important if you use Graphite. As the order of common tags cannot be guaranteed by using this approach, Graphite users are advised to define a custom `MeterFilter` instead. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index b7db70d502..f077a1409c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -9,9 +9,9 @@ To create your own observations (which will lead to metrics and traces), you can include::code:MyCustomObservation[] -NOTE: Low cardinality tags will be added to metrics and traces, while high cardinality tags will only be added to traces. +NOTE: Low cardinality key-values will be added to metrics and traces, while high cardinality key-values will only be added to traces. -Beans of type `ObservationPredicate`, `GlobalObservationConvention` and `ObservationHandler` will be automatically registered on the `ObservationRegistry`. +Beans of type `ObservationPredicate`, `GlobalObservationConvention`, `ObservationFilter` and `ObservationHandler` will be automatically registered on the `ObservationRegistry`. You can additionally register any number of `ObservationRegistryCustomizer` beans to further configure the registry. For more details please see the https://micrometer.io/docs/observation[Micrometer Observation documentation]. @@ -21,4 +21,20 @@ For JDBC, the https://github.com/jdbc-observations/datasource-micrometer[Datasou Read more about it https://jdbc-observations.github.io/datasource-micrometer/docs/current/docs/html/[in the reference documentation]. For R2DBC, the https://github.com/spring-projects-experimental/r2dbc-micrometer-spring-boot[Spring Boot Auto Configuration for R2DBC Observation] creates observations for R2DBC query invocations. +[[actuator.observability.common-key-values]] +=== Common Key-Values +Common key-values are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. +Commons key-values are applied to all observations as low cardinality key-values and can be configured, as the following example shows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + observations: + key-values: + region: "us-east-1" + stack: "prod" +---- + +The preceding example adds `region` and `stack` key-values to all observations with a value of `us-east-1` and `prod`, respectively. + The next sections will provide more details about logging, metrics and traces.