mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-06-30 00:36:49 +08:00
Merge pull request #40961 from making
* pr/40961: Polish "Provide auto configuration for OpenTelemetry Logs" Provide auto configuration for OpenTelemetry Logs Closes gh-40961
This commit is contained in:
commit
86ffb68a10
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.sdk.logs.LogRecordProcessor;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
|
||||
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
|
||||
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry logging.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class })
|
||||
public class OpenTelemetryLoggingAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
BatchLogRecordProcessor batchLogRecordProcessor(ObjectProvider<LogRecordExporter> logRecordExporters) {
|
||||
return BatchLogRecordProcessor.builder(LogRecordExporter.composite(logRecordExporters.orderedStream().toList()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
SdkLoggerProvider otelSdkLoggerProvider(Resource resource, ObjectProvider<LogRecordProcessor> logRecordProcessors,
|
||||
ObjectProvider<SdkLoggerProviderBuilderCustomizer> customizers) {
|
||||
SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().setResource(resource);
|
||||
logRecordProcessors.orderedStream().forEach(builder::addLogRecordProcessor);
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry;
|
||||
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize the {@link SdkLoggerProviderBuilder}
|
||||
* that is used to create the auto-configured {@link SdkLoggerProvider}.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SdkLoggerProviderBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the given {@code builder}.
|
||||
* @param builder the builder to customize
|
||||
*/
|
||||
void customize(SdkLoggerProviderBuilder builder);
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry.otlp;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for OTLP logging.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class, OtlpHttpLogRecordExporter.class })
|
||||
@EnableConfigurationProperties(OtlpLoggingProperties.class)
|
||||
@Import({ OtlpLoggingConfigurations.ConnectionDetails.class, OtlpLoggingConfigurations.Exporters.class })
|
||||
public class OtlpLoggingAutoConfiguration {
|
||||
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry.otlp;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
|
||||
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configurations imported by {@link OtlpLoggingAutoConfiguration}.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
final class OtlpLoggingConfigurations {
|
||||
|
||||
private OtlpLoggingConfigurations() {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ConnectionDetails {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = "management.otlp.logging", name = "endpoint")
|
||||
OtlpLoggingConnectionDetails otlpLogsConnectionDetails(OtlpLoggingProperties properties) {
|
||||
return new PropertiesOtlpLoggingConnectionDetails(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts {@link OtlpLoggingProperties} to {@link OtlpLoggingConnectionDetails}.
|
||||
*/
|
||||
static class PropertiesOtlpLoggingConnectionDetails implements OtlpLoggingConnectionDetails {
|
||||
|
||||
private final OtlpLoggingProperties properties;
|
||||
|
||||
PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEndpoint() {
|
||||
return this.properties.getEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Exporters {
|
||||
|
||||
@ConditionalOnMissingBean(value = OtlpHttpLogRecordExporter.class,
|
||||
type = "io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter")
|
||||
@ConditionalOnBean(OtlpLoggingConnectionDetails.class)
|
||||
@Bean
|
||||
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties,
|
||||
OtlpLoggingConnectionDetails connectionDetails) {
|
||||
OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder()
|
||||
.setEndpoint(connectionDetails.getEndpoint())
|
||||
.setCompression(properties.getCompression().name().toLowerCase(Locale.US))
|
||||
.setTimeout(properties.getTimeout());
|
||||
properties.getHeaders().forEach(builder::addHeader);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry.otlp;
|
||||
|
||||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||
|
||||
/**
|
||||
* Details required to establish a connection to an OpenTelemetry logging service.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public interface OtlpLoggingConnectionDetails extends ConnectionDetails {
|
||||
|
||||
/**
|
||||
* Address to where logs will be published.
|
||||
* @return the address to where logs will be published
|
||||
*/
|
||||
String getEndpoint();
|
||||
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry.otlp;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration properties for exporting logs using OTLP.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@ConfigurationProperties("management.otlp.logging")
|
||||
public class OtlpLoggingProperties {
|
||||
|
||||
/**
|
||||
* URL to the OTel collector's HTTP API.
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* Call timeout for the OTel Collector to process an exported batch of data. This
|
||||
* timeout spans the entire call: resolving DNS, connecting, writing the request body,
|
||||
* server processing, and reading the response body. If the call requires redirects or
|
||||
* retries all must complete within one timeout period.
|
||||
*/
|
||||
private Duration timeout = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* Method used to compress the payload.
|
||||
*/
|
||||
private Compression compression = Compression.NONE;
|
||||
|
||||
/**
|
||||
* Custom HTTP headers you want to pass to the collector, for example auth headers.
|
||||
*/
|
||||
private final Map<String, String> headers = new HashMap<>();
|
||||
|
||||
public String getEndpoint() {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Compression getCompression() {
|
||||
return this.compression;
|
||||
}
|
||||
|
||||
public void setCompression(Compression compression) {
|
||||
this.compression = compression;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
public enum Compression {
|
||||
|
||||
/**
|
||||
* Gzip compression.
|
||||
*/
|
||||
GZIP,
|
||||
|
||||
/**
|
||||
* No compression.
|
||||
*/
|
||||
NONE
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for OpenTelemetry logging with OTLP.
|
||||
*/
|
||||
package org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.otlp;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2012-2019 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for OpenTelemetry logging.
|
||||
*/
|
||||
package org.springframework.boot.actuate.autoconfigure.logging.opentelemetry;
|
|
@ -35,6 +35,8 @@ org.springframework.boot.actuate.autoconfigure.ldap.LdapHealthContributorAutoCon
|
|||
org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.OpenTelemetryLoggingAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.otlp.OtlpLoggingAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.mail.MailHealthContributorAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.sdk.common.CompletableResultCode;
|
||||
import io.opentelemetry.sdk.logs.LogRecordProcessor;
|
||||
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
|
||||
import io.opentelemetry.sdk.logs.data.LogRecordData;
|
||||
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
|
||||
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenTelemetryLoggingAutoConfiguration}.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
class OpenTelemetryLoggingAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner;
|
||||
|
||||
OpenTelemetryLoggingAutoConfigurationTests() {
|
||||
this.contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
|
||||
org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class,
|
||||
OpenTelemetryLoggingAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupplyBeans() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
|
||||
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "io.opentelemetry.sdk.logs", "io.opentelemetry.api" })
|
||||
void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> {
|
||||
assertThat(context).doesNotHaveBean(BatchLogRecordProcessor.class);
|
||||
assertThat(context).doesNotHaveBean(SdkLoggerProvider.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackOffOnCustomBeans() {
|
||||
this.contextRunner.withUserConfiguration(CustomConfig.class).run((context) -> {
|
||||
assertThat(context).hasBean("customBatchLogRecordProcessor").hasSingleBean(BatchLogRecordProcessor.class);
|
||||
assertThat(context.getBeansOfType(LogRecordProcessor.class)).hasSize(1);
|
||||
assertThat(context).hasBean("customSdkLoggerProvider").hasSingleBean(SdkLoggerProvider.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowMultipleLogRecordExporter() {
|
||||
this.contextRunner.withUserConfiguration(MultipleLogRecordExporterConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
|
||||
assertThat(context.getBeansOfType(LogRecordExporter.class)).hasSize(2);
|
||||
assertThat(context).hasBean("customLogRecordExporter1");
|
||||
assertThat(context).hasBean("customLogRecordExporter2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowMultipleLogRecordProcessorInAdditionToBatchLogRecordProcessor() {
|
||||
this.contextRunner.withUserConfiguration(MultipleLogRecordProcessorConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
|
||||
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
|
||||
assertThat(context.getBeansOfType(LogRecordProcessor.class)).hasSize(3);
|
||||
assertThat(context).hasBean("batchLogRecordProcessor");
|
||||
assertThat(context).hasBean("customLogRecordProcessor1");
|
||||
assertThat(context).hasBean("customLogRecordProcessor2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowMultipleSdkLoggerProviderBuilderCustomizer() {
|
||||
this.contextRunner.withUserConfiguration(MultipleSdkLoggerProviderBuilderCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
|
||||
assertThat(context.getBeansOfType(NoopSdkLoggerProviderBuilderCustomizer.class)).hasSize(2);
|
||||
assertThat(context).hasBean("customSdkLoggerProviderBuilderCustomizer1");
|
||||
assertThat(context).hasBean("customSdkLoggerProviderBuilderCustomizer2");
|
||||
assertThat(context
|
||||
.getBean("customSdkLoggerProviderBuilderCustomizer1", NoopSdkLoggerProviderBuilderCustomizer.class)
|
||||
.called()).isEqualTo(1);
|
||||
assertThat(context
|
||||
.getBean("customSdkLoggerProviderBuilderCustomizer2", NoopSdkLoggerProviderBuilderCustomizer.class)
|
||||
.called()).isEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class CustomConfig {
|
||||
|
||||
@Bean
|
||||
public BatchLogRecordProcessor customBatchLogRecordProcessor() {
|
||||
return BatchLogRecordProcessor.builder(new NoopLogRecordExporter()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SdkLoggerProvider customSdkLoggerProvider() {
|
||||
return SdkLoggerProvider.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class MultipleLogRecordExporterConfig {
|
||||
|
||||
@Bean
|
||||
public LogRecordExporter customLogRecordExporter1() {
|
||||
return new NoopLogRecordExporter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogRecordExporter customLogRecordExporter2() {
|
||||
return new NoopLogRecordExporter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class MultipleLogRecordProcessorConfig {
|
||||
|
||||
@Bean
|
||||
public LogRecordProcessor customLogRecordProcessor1() {
|
||||
return new NoopLogRecordProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogRecordProcessor customLogRecordProcessor2() {
|
||||
return new NoopLogRecordProcessor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class MultipleSdkLoggerProviderBuilderCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer1() {
|
||||
return new NoopSdkLoggerProviderBuilderCustomizer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer2() {
|
||||
return new NoopSdkLoggerProviderBuilderCustomizer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NoopLogRecordExporter implements LogRecordExporter {
|
||||
|
||||
@Override
|
||||
public CompletableResultCode export(Collection<LogRecordData> logs) {
|
||||
return CompletableResultCode.ofSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableResultCode flush() {
|
||||
return CompletableResultCode.ofSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableResultCode shutdown() {
|
||||
return CompletableResultCode.ofSuccess();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NoopLogRecordProcessor implements LogRecordProcessor {
|
||||
|
||||
@Override
|
||||
public void onEmit(Context context, ReadWriteLogRecord logRecord) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NoopSdkLoggerProviderBuilderCustomizer implements SdkLoggerProviderBuilderCustomizer {
|
||||
|
||||
final AtomicInteger called = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public void customize(SdkLoggerProviderBuilder builder) {
|
||||
this.called.incrementAndGet();
|
||||
}
|
||||
|
||||
int called() {
|
||||
return this.called.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry.otlp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.opentelemetry.api.logs.Severity;
|
||||
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import okio.Buffer;
|
||||
import okio.GzipSource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.OpenTelemetryLoggingAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link OtlpLoggingAutoConfiguration}.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public class OtlpLoggingAutoConfigurationIntegrationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.application.name=otlp-logs-test",
|
||||
"management.otlp.logging.headers.Authorization=Bearer my-token")
|
||||
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
|
||||
OpenTelemetryLoggingAutoConfiguration.class, OtlpLoggingAutoConfiguration.class));
|
||||
|
||||
private final MockWebServer mockWebServer = new MockWebServer();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.mockWebServer.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.mockWebServer.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpLogRecordExporterShouldUseProtobufAndNoCompressionByDefault() {
|
||||
this.mockWebServer.enqueue(new MockResponse());
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:%d/v1/logs"
|
||||
.formatted(this.mockWebServer.getPort()))
|
||||
.run((context) -> {
|
||||
logMessage(context);
|
||||
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
|
||||
assertThat(request).isNotNull();
|
||||
assertThat(request.getRequestLine()).contains("/v1/logs");
|
||||
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
|
||||
assertThat(request.getHeader("Content-Encoding")).isNull();
|
||||
assertThat(request.getBodySize()).isPositive();
|
||||
try (Buffer body = request.getBody()) {
|
||||
assertLogMessage(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpLogRecordExporterCanBeConfiguredToUseGzipCompression() {
|
||||
this.mockWebServer.enqueue(new MockResponse());
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:%d/v1/logs"
|
||||
.formatted(this.mockWebServer.getPort()), "management.otlp.logging.compression=gzip")
|
||||
.run((context) -> {
|
||||
logMessage(context);
|
||||
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
|
||||
assertThat(request).isNotNull();
|
||||
assertThat(request.getRequestLine()).contains("/v1/logs");
|
||||
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
|
||||
assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip");
|
||||
assertThat(request.getBodySize()).isPositive();
|
||||
try (Buffer uncompressed = new Buffer(); Buffer body = request.getBody()) {
|
||||
uncompressed.writeAll(new GzipSource(body));
|
||||
assertLogMessage(uncompressed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void logMessage(ApplicationContext context) {
|
||||
SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class);
|
||||
loggerProvider.get("test")
|
||||
.logRecordBuilder()
|
||||
.setSeverity(Severity.INFO)
|
||||
.setSeverityText("INFO")
|
||||
.setBody("Hello")
|
||||
.setTimestamp(Instant.now())
|
||||
.emit();
|
||||
}
|
||||
|
||||
private static void assertLogMessage(Buffer body) {
|
||||
String string = body.readString(StandardCharsets.UTF_8);
|
||||
assertThat(string).contains("otlp-logs-test");
|
||||
assertThat(string).contains("test");
|
||||
assertThat(string).contains("INFO");
|
||||
assertThat(string).contains("Hello");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright 2012-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.
|
||||
* 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.logging.opentelemetry.otlp;
|
||||
|
||||
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
|
||||
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
|
||||
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
|
||||
import okhttp3.HttpUrl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OtlpLoggingAutoConfiguration}.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
class OtlpLoggingAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void shouldNotSupplyBeansIfPropertyIsNotSet() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class);
|
||||
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupplyBeans() {
|
||||
this.contextRunner.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class);
|
||||
OtlpLoggingConnectionDetails connectionDetails = context.getBean(OtlpLoggingConnectionDetails.class);
|
||||
assertThat(connectionDetails.getEndpoint()).isEqualTo("http://localhost:4318/v1/logs");
|
||||
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class)
|
||||
.hasSingleBean(LogRecordExporter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "io.opentelemetry.sdk.logs", "io.opentelemetry.api",
|
||||
"io.opentelemetry.exporter.otlp.http.logs" })
|
||||
void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> {
|
||||
assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class);
|
||||
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackOffWhenCustomHttpExporterIsDefined() {
|
||||
this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class)
|
||||
.run((context) -> assertThat(context).hasBean("customOtlpHttpLogRecordExporter")
|
||||
.hasSingleBean(LogRecordExporter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackOffWhenCustomGrpcExporterIsDefined() {
|
||||
this.contextRunner.withUserConfiguration(CustomGrpcExporterConfiguration.class)
|
||||
.run((context) -> assertThat(context).hasBean("customOtlpGrpcLogRecordExporter")
|
||||
.hasSingleBean(LogRecordExporter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackOffWhenCustomOtlpLogsConnectionDetailsIsDefined() {
|
||||
this.contextRunner.withUserConfiguration(CustomOtlpLogsConnectionDetails.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class)
|
||||
.doesNotHaveBean(PropertiesOtlpLoggingConnectionDetails.class);
|
||||
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter = context.getBean(OtlpHttpLogRecordExporter.class);
|
||||
assertThat(otlpHttpLogRecordExporter).extracting("delegate.httpSender.url")
|
||||
.isEqualTo(HttpUrl.get("https://otel.example.com/v1/logs"));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class CustomHttpExporterConfiguration {
|
||||
|
||||
@Bean
|
||||
public OtlpHttpLogRecordExporter customOtlpHttpLogRecordExporter() {
|
||||
return OtlpHttpLogRecordExporter.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class CustomGrpcExporterConfiguration {
|
||||
|
||||
@Bean
|
||||
public OtlpGrpcLogRecordExporter customOtlpGrpcLogRecordExporter() {
|
||||
return OtlpGrpcLogRecordExporter.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class CustomOtlpLogsConnectionDetails {
|
||||
|
||||
@Bean
|
||||
public OtlpLoggingConnectionDetails customOtlpLogsConnectionDetails() {
|
||||
return () -> "https://otel.example.com/v1/logs";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user