Merge pull request #41551 from eddumelendez

* pr/41551:
  Polish "Add support for Grafana LGTM stack"
  Add support for Grafana LGTM stack

Closes gh-41551
This commit is contained in:
Stéphane Nicoll 2024-07-29 10:56:07 +02:00
commit 3da45aabbf
16 changed files with 419 additions and 12 deletions

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.docker.compose.service.connection.otlp;
import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link OpenTelemetryMetricsDockerComposeConnectionDetailsFactory}
* using {@link TestImage#GRAFANA_OTEL_LGTM}.
*
* @author Eddú Meléndez
*/
class GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests {
@DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM)
void runCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics");
}
}

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.docker.compose.service.connection.otlp;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link OpenTelemetryTracingDockerComposeConnectionDetailsFactory}
* using {@link TestImage#GRAFANA_OTEL_LGTM}.
*
* @author Eddú Meléndez
*/
class GrafanaOpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests {
@DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM)
void runCreatesConnectionDetails(OtlpTracingConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/traces");
}
}

View File

@ -23,8 +23,8 @@ import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for
* {@link OpenTelemetryMetricsDockerComposeConnectionDetailsFactory}.
* Integration tests for {@link OpenTelemetryMetricsDockerComposeConnectionDetailsFactory}
* using {@link TestImage#OPENTELEMETRY}.
*
* @author Eddú Meléndez
*/

View File

@ -23,8 +23,8 @@ import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for
* {@link OpenTelemetryTracingDockerComposeConnectionDetailsFactory}.
* Integration tests for {@link OpenTelemetryTracingDockerComposeConnectionDetailsFactory}
* using {@link TestImage#OPENTELEMETRY}.
*
* @author Eddú Meléndez
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* 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.
@ -30,10 +30,13 @@ import org.springframework.boot.docker.compose.service.connection.DockerComposeC
class OpenTelemetryMetricsDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<OtlpMetricsConnectionDetails> {
private static final String[] OPENTELEMETRY_IMAGE_NAMES = { "otel/opentelemetry-collector-contrib",
"grafana/otel-lgtm" };
private static final int OTLP_PORT = 4318;
OpenTelemetryMetricsDockerComposeConnectionDetailsFactory() {
super("otel/opentelemetry-collector-contrib",
super(OPENTELEMETRY_IMAGE_NAMES,
"org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* 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.
@ -30,10 +30,13 @@ import org.springframework.boot.docker.compose.service.connection.DockerComposeC
class OpenTelemetryTracingDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<OtlpTracingConnectionDetails> {
private static final String[] OPENTELEMETRY_IMAGE_NAMES = { "otel/opentelemetry-collector-contrib",
"grafana/otel-lgtm" };
private static final int OTLP_PORT = 4318;
OpenTelemetryTracingDockerComposeConnectionDetailsFactory() {
super("otel/opentelemetry-collector-contrib",
super(OPENTELEMETRY_IMAGE_NAMES,
"org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration");
}

View File

@ -108,10 +108,10 @@ The following service connections are currently supported:
| Containers named "neo4j" or "bitnami/neo4j"
| `OtlpMetricsConnectionDetails`
| Containers named "otel/opentelemetry-collector-contrib"
| Containers named "otel/opentelemetry-collector-contrib", "grafana/otel-lgtm"
| `OtlpTracingConnectionDetails`
| Containers named "otel/opentelemetry-collector-contrib"
| Containers named "otel/opentelemetry-collector-contrib", "grafana/otel-lgtm"
| `PulsarConnectionDetails`
| Containers named "apachepulsar/pulsar"

View File

@ -72,10 +72,10 @@ The following service connection factories are provided in the `spring-boot-test
| Containers of type `Neo4jContainer`
| `OtlpMetricsConnectionDetails`
| Containers named "otel/opentelemetry-collector-contrib"
| Containers named "otel/opentelemetry-collector-contrib" or of type `LgtmStackContainer`
| `OtlpTracingConnectionDetails`
| Containers named "otel/opentelemetry-collector-contrib"
| Containers named "otel/opentelemetry-collector-contrib" or of type `LgtmStackContainer`
| `PulsarConnectionDetails`
| Containers of type `PulsarContainer`

View File

@ -66,6 +66,7 @@ dependencies {
optional("org.testcontainers:cassandra")
optional("org.testcontainers:couchbase")
optional("org.testcontainers:elasticsearch")
optional("org.testcontainers:grafana")
optional("org.testcontainers:jdbc")
optional("org.testcontainers:kafka")
optional("org.testcontainers:mariadb")

View File

@ -0,0 +1,129 @@
/*
* 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.testcontainers.service.connection.otlp;
import java.time.Duration;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.testcontainers.grafana.LgtmStackContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory}.
*
* @author Eddú Meléndez
*/
@SpringJUnitConfig
@TestPropertySource(properties = { "management.otlp.metrics.export.resource-attributes.service.name=test",
"management.otlp.metrics.export.step=1s" })
@Testcontainers(disabledWithoutDocker = true)
class GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests {
@Container
@ServiceConnection
static final LgtmStackContainer container = TestImage.container(LgtmStackContainer.class);
@Autowired
private MeterRegistry meterRegistry;
@Test
void connectionCanBeMadeToOpenTelemetryCollectorContainer() {
Counter.builder("test.counter").register(this.meterRegistry).increment(42);
Gauge.builder("test.gauge", () -> 12).register(this.meterRegistry);
Timer.builder("test.timer").register(this.meterRegistry).record(Duration.ofMillis(123));
DistributionSummary.builder("test.distributionsummary").register(this.meterRegistry).record(24);
Awaitility.given()
.pollInterval(Duration.ofSeconds(2))
.atMost(Duration.ofSeconds(10))
.ignoreExceptions()
.untilAsserted(() -> {
Response response = RestAssured.given()
.queryParam("query", "{job=\"test\"}")
.get("%s/api/v1/query".formatted(container.getPromehteusHttpUrl()))
.prettyPeek()
.thenReturn();
assertThat(response.getStatusCode()).isEqualTo(200);
assertThat(response.body()
.jsonPath()
.getList("data.result.find { it.metric.__name__ == 'test_counter_total' }.value")).contains("42");
assertThat(response.body()
.jsonPath()
.getList("data.result.find { it.metric.__name__ == 'test_gauge' }.value")).contains("12");
assertThat(response.body()
.jsonPath()
.getList("data.result.find { it.metric.__name__ == 'test_timer_milliseconds_count' }.value"))
.contains("1");
assertThat(response.body()
.jsonPath()
.getList("data.result.find { it.metric.__name__ == 'test_timer_milliseconds_sum' }.value"))
.contains("123");
assertThat(response.body()
.jsonPath()
.getList(
"data.result.find { it.metric.__name__ == 'test_timer_milliseconds_bucket' & it.metric.le == '+Inf' }.value"))
.contains("1");
assertThat(response.body()
.jsonPath()
.getList("data.result.find { it.metric.__name__ == 'test_distributionsummary_count' }.value"))
.contains("1");
assertThat(response.body()
.jsonPath()
.getList("data.result.find { it.metric.__name__ == 'test_distributionsummary_sum' }.value"))
.contains("24");
assertThat(response.body()
.jsonPath()
.getList(
"data.result.find { it.metric.__name__ == 'test_distributionsummary_bucket' & it.metric.le == '+Inf' }.value"))
.contains("1");
});
}
@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration(OtlpMetricsExportAutoConfiguration.class)
static class TestConfiguration {
@Bean
Clock customClock() {
return Clock.SYSTEM;
}
}
}

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.testcontainers.service.connection.otlp;
import org.junit.jupiter.api.Test;
import org.testcontainers.grafana.LgtmStackContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory}.
*
* @author Eddú Meléndez
*/
@SpringJUnitConfig
@Testcontainers(disabledWithoutDocker = true)
class GrafanaOpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests {
@Container
@ServiceConnection
static final LgtmStackContainer container = TestImage.container(LgtmStackContainer.class);
@Autowired
private OtlpTracingConnectionDetails connectionDetails;
@Test
void connectionCanBeMadeToOpenTelemetryContainer() {
assertThat(this.connectionDetails.getUrl()).isEqualTo("%s/v1/traces".formatted(container.getOtlpHttpUrl()));
}
@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration(OtlpAutoConfiguration.class)
static class TestConfiguration {
}
}

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.testcontainers.service.connection.otlp;
import org.testcontainers.grafana.LgtmStackContainer;
import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} to create
* {@link OtlpMetricsConnectionDetails} from a
* {@link ServiceConnection @ServiceConnection}-annotated {@link LgtmStackContainer} using
* the {@code "grafana/otel-lgtmb"} image.
*
* @author Eddú Meléndez
*/
class GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<LgtmStackContainer, OtlpMetricsConnectionDetails> {
GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME,
"org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration");
}
@Override
protected OtlpMetricsConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<LgtmStackContainer> source) {
return new OpenTelemetryMetricsContainerConnectionDetails(source);
}
private static final class OpenTelemetryMetricsContainerConnectionDetails
extends ContainerConnectionDetails<LgtmStackContainer> implements OtlpMetricsConnectionDetails {
private OpenTelemetryMetricsContainerConnectionDetails(ContainerConnectionSource<LgtmStackContainer> source) {
super(source);
}
@Override
public String getUrl() {
return "%s/v1/metrics".formatted(getContainer().getOtlpHttpUrl());
}
}
}

View File

@ -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.testcontainers.service.connection.otlp;
import org.testcontainers.grafana.LgtmStackContainer;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} to create
* {@link OtlpTracingConnectionDetails} from a
* {@link ServiceConnection @ServiceConnection}-annotated {@link LgtmStackContainer} using
* the {@code "grafana/otel-lgtm"} image.
*
* @author Eddú Meléndez
*/
class GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<LgtmStackContainer, OtlpTracingConnectionDetails> {
GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME, "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration");
}
@Override
protected OtlpTracingConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<LgtmStackContainer> source) {
return new OpenTelemetryTracingContainerConnectionDetails(source);
}
private static final class OpenTelemetryTracingContainerConnectionDetails
extends ContainerConnectionDetails<LgtmStackContainer> implements OtlpTracingConnectionDetails {
private OpenTelemetryTracingContainerConnectionDetails(ContainerConnectionSource<LgtmStackContainer> source) {
super(source);
}
@Override
public String getUrl() {
return "%s/v1/traces".formatted(getContainer().getOtlpHttpUrl());
}
}
}

View File

@ -23,6 +23,8 @@ org.springframework.boot.testcontainers.service.connection.ldap.OpenLdapContaine
org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.otlp.GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.otlp.GrafanaOpenTelemetryTracingContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryMetricsContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryTracingContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.pulsar.PulsarContainerConnectionDetailsFactory,\

View File

@ -20,6 +20,7 @@ dependencies {
optional("org.testcontainers:cassandra")
optional("org.testcontainers:couchbase")
optional("org.testcontainers:elasticsearch")
optional("org.testcontainers:grafana")
optional("org.testcontainers:junit-jupiter")
optional("org.testcontainers:kafka")
optional("org.testcontainers:mongodb")

View File

@ -36,6 +36,7 @@ import org.testcontainers.containers.PulsarContainer;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.couchbase.CouchbaseContainer;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.grafana.LgtmStackContainer;
import org.testcontainers.redpanda.RedpandaContainer;
import org.testcontainers.utility.DockerImageName;
@ -100,6 +101,12 @@ public enum TestImage {
*/
ELASTICSEARCH_8("elasticsearch", "8.6.1"),
/**
* A container image suitable for testing Grafana OTel LGTM.
*/
GRAFANA_OTEL_LGTM("grafana/otel-lgtm", "0.6.0", () -> LgtmStackContainer.class,
(container) -> ((LgtmStackContainer) container).withStartupTimeout(Duration.ofMinutes(2))),
/**
* A container image suitable for testing Confluent's distribution of Kafka.
*/