Browse Source
* pr/40961: Polish "Provide auto configuration for OpenTelemetry Logs" Provide auto configuration for OpenTelemetry Logs Closes gh-40961pull/41323/head
12 changed files with 891 additions and 0 deletions
@ -0,0 +1,61 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
||||
@ -0,0 +1,226 @@
@@ -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 @@
@@ -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 @@
@@ -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