Provide auto configuration for OpenTelemetry Logs

See gh-40961
This commit is contained in:
Toshiaki Maki 2024-05-31 15:41:39 +09:00 committed by Moritz Halbritter
parent 895fbd7057
commit 2d6f2488b7
12 changed files with 896 additions and 0 deletions

View File

@ -0,0 +1,62 @@
/*
* 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.logs;
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 Logs.
*
* @author Toshiaki Maki
* @since 3.4.0
*/
@AutoConfiguration("openTelemetryLogsAutoConfiguration")
@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class })
public class OpenTelemetryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public BatchLogRecordProcessor batchLogRecordProcessor(ObjectProvider<LogRecordExporter> logRecordExporters) {
return BatchLogRecordProcessor.builder(LogRecordExporter.composite(logRecordExporters.orderedStream().toList()))
.build();
}
@Bean
@ConditionalOnMissingBean
public 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();
}
}

View File

@ -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.logs;
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);
}

View File

@ -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.logs.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 Logs.
*
* @author Toshiaki Maki
* @since 3.4.0
*/
@AutoConfiguration
@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class, OtlpHttpLogRecordExporter.class })
@EnableConfigurationProperties(OtlpProperties.class)
@Import({ OtlpLogsConfigurations.ConnectionDetails.class, OtlpLogsConfigurations.Exporters.class })
public class OtlpLogsAutoConfiguration {
}

View File

@ -0,0 +1,87 @@
/*
* 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.logs.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 OtlpLogsAutoConfiguration}.
*
* @author Toshiaki Maki
* @since 3.4.0
*/
public class OtlpLogsConfigurations {
@Configuration(proxyBeanMethods = false)
static class ConnectionDetails {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "management.otlp.logs", name = "endpoint")
OtlpLogsConnectionDetails otlpLogsConnectionDetails(OtlpProperties properties) {
return new PropertiesOtlpLogsConnectionDetails(properties);
}
/**
* Adapts {@link OtlpProperties} to {@link OtlpLogsConnectionDetails}.
*/
static class PropertiesOtlpLogsConnectionDetails implements OtlpLogsConnectionDetails {
private final OtlpProperties properties;
PropertiesOtlpLogsConnectionDetails(OtlpProperties properties) {
this.properties = properties;
}
@Override
public String getUrl() {
return this.properties.getEndpoint();
}
}
}
@Configuration(proxyBeanMethods = false)
static class Exporters {
@ConditionalOnMissingBean(value = OtlpHttpLogRecordExporter.class,
type = "io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter")
@ConditionalOnBean(OtlpLogsConnectionDetails.class)
@Bean
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpProperties properties,
OtlpLogsConnectionDetails connectionDetails) {
OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder()
.setEndpoint(connectionDetails.getUrl())
.setCompression(properties.getCompression().name().toLowerCase(Locale.US))
.setTimeout(properties.getTimeout());
properties.getHeaders().forEach(builder::addHeader);
return builder.build();
}
}
}

View File

@ -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.logs.otlp;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
/**
* Details required to establish a connection to an OpenTelemetry logs service.
*
* @author Toshiaki Maki
* @since 3.4.0
*/
public interface OtlpLogsConnectionDetails extends ConnectionDetails {
/**
* Address to where logs will be published.
* @return the address to where logs will be published
*/
String getUrl();
}

View File

@ -0,0 +1,103 @@
/*
* 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.logs.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.logs")
public class OtlpProperties {
/**
* 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 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 void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public enum Compression {
/**
* Gzip compression.
*/
GZIP,
/**
* No compression.
*/
NONE
}
}

View File

@ -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 logs with OTLP.
*/
package org.springframework.boot.actuate.autoconfigure.logs.otlp;

View File

@ -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 Logs.
*/
package org.springframework.boot.actuate.autoconfigure.logs;

View File

@ -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.logs.OpenTelemetryAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logs.otlp.OtlpLogsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.mail.MailHealthContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration

View File

@ -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.logs;
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 OpenTelemetryAutoConfiguration}.
*
* @author Toshiaki Maki
*/
class OpenTelemetryAutoConfigurationTests {
private final ApplicationContextRunner contextRunner;
OpenTelemetryAutoConfigurationTests() {
this.contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(
org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class,
OpenTelemetryAutoConfiguration.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();
}
}
}

View File

@ -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.logs.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.logs.OpenTelemetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link OtlpLogsAutoConfiguration}.
*
* @author Toshiaki Maki
*/
public class OtlpLogsAutoConfigurationIntegrationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.application.name=otlp-logs-test",
"management.otlp.logs.headers.Authorization=Bearer my-token")
.withConfiguration(AutoConfigurations.of(
org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class,
OpenTelemetryAutoConfiguration.class, OtlpLogsAutoConfiguration.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.logs.endpoint=http://localhost:%d/v1/logs".formatted(this.mockWebServer.getPort()))
.run((context) -> {
SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class);
loggerProvider.get("test")
.logRecordBuilder()
.setSeverity(Severity.INFO)
.setSeverityText("INFO")
.setBody("Hello")
.setTimestamp(Instant.now())
.emit();
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()) {
String bodyString = body.readString(StandardCharsets.UTF_8);
assertThat(bodyString).contains("otlp-logs-test");
assertThat(bodyString).contains("test");
assertThat(bodyString).contains("INFO");
assertThat(bodyString).contains("Hello");
}
});
}
@Test
void httpLogRecordExporterCanBeConfiguredToUseGzipCompression() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues(
"management.otlp.logs.endpoint=http://localhost:%d/v1/logs".formatted(this.mockWebServer.getPort()),
"management.otlp.logs.compression=gzip")
.run((context) -> {
SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class);
loggerProvider.get("test")
.logRecordBuilder()
.setBody("Hello")
.setSeverity(Severity.INFO)
.setSeverityText("INFO")
.setTimestamp(Instant.now())
.emit();
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));
String bodyString = uncompressed.readString(StandardCharsets.UTF_8);
assertThat(bodyString).contains("otlp-logs-test");
assertThat(bodyString).contains("test");
assertThat(bodyString).contains("INFO");
assertThat(bodyString).contains("Hello");
}
});
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.logs.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.logs.otlp.OtlpLogsConfigurations.ConnectionDetails.PropertiesOtlpLogsConnectionDetails;
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 OtlpLogsAutoConfiguration}.
*
* @author Toshiaki Maki
*/
class OtlpLogsAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OtlpLogsAutoConfiguration.class));
@Test
void shouldNotSupplyBeansIfPropertyIsNotSet() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(OtlpLogsConnectionDetails.class);
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
});
}
@Test
void shouldSupplyBeans() {
this.contextRunner.withPropertyValues("management.otlp.logs.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpLogsConnectionDetails.class);
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(OtlpLogsConnectionDetails.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(OtlpLogsConnectionDetails.class)
.doesNotHaveBean(PropertiesOtlpLogsConnectionDetails.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 OtlpLogsConnectionDetails customOtlpLogsConnectionDetails() {
return () -> "https://otel.example.com/v1/logs";
}
}
}