Added OpenTelemetry to CPS 67/137767/11
authordavid.mcweeney <david.mcweeney@est.tech>
Thu, 25 Apr 2024 13:37:33 +0000 (14:37 +0100)
committerdavid.mcweeney <david.mcweeney@est.tech>
Fri, 31 May 2024 13:43:01 +0000 (14:43 +0100)
Change-Id: I192fa53e293ea43cdff92ebd44d0382eb290bb76
Signed-off-by: david.mcweeney <david.mcweeney@est.tech>
Issue-ID: CPS-2172

cps-application/pom.xml
cps-application/src/main/resources/application.yml
cps-dependencies/pom.xml
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfig.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfig.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfigSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfigSpec.groovy
integration-test/src/test/resources/application.yml

index 6804c7d..abcb88f 100644 (file)
@@ -75,7 +75,7 @@
         </dependency>
         <dependency>
             <groupId>io.micrometer</groupId>
-            <artifactId>micrometer-tracing-bridge-brave</artifactId>
+            <artifactId>micrometer-tracing-bridge-otel</artifactId>
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.dataformat</groupId>
index e724ef4..9c8c1ec 100644 (file)
@@ -151,8 +151,23 @@ security:
         username: ${CPS_USERNAME:cpsuser}
         password: ${CPS_PASSWORD:cpsr0cks!}
 
+cps:
+    tracing:
+        sampler:
+            jaeger_remote:
+                endpoint: ${ONAP_OTEL_SAMPLER_JAEGER_REMOTE_ENDPOINT:http://onap-otel-collector:14250}
+        exporter:
+            endpoint: ${ONAP_OTEL_EXPORTER_ENDPOINT:http://onap-otel-collector:4317}
+            protocol: ${ONAP_OTEL_EXPORTER_PROTOCOL:grpc}
+        enabled: ${ONAP_TRACING_ENABLED:false}
+
 # Actuator
 management:
+    tracing:
+        propagation:
+            produce: ${ONAP_PROPAGATOR_PRODUCE:[W3C]}
+        sampling:
+            probability: 1.0
     endpoints:
         web:
             exposure:
@@ -214,3 +229,9 @@ hazelcast:
         kubernetes:
             enabled: ${HAZELCAST_MODE_KUBERNETES_ENABLED:false}
             service-name: ${CPS_NCMP_SERVICE_NAME:"cps-and-ncmp-service"}
+
+otel:
+    exporter:
+        otlp:
+            traces:
+                protocol: ${ONAP_OTEL_EXPORTER_OTLP_TRACES_PROTOCOL:grpc}
\ No newline at end of file
index f323cd7..a50a420 100644 (file)
@@ -42,6 +42,7 @@
         <testcontainers.version>1.18.3</testcontainers.version>
         <mapstruct.version>1.4.2.Final</mapstruct.version>
         <jetty-version>11.0.16</jetty-version>
+        <version.opentelemetry-instrumentation-bom>2.1.0-alpha</version.opentelemetry-instrumentation-bom>
     </properties>
 
     <build>
                 <version>3.7.3</version>
             </dependency>
             <dependency>
-                <groupId>io.micrometer</groupId>
-                <artifactId>micrometer-tracing-bridge-brave</artifactId>
-                <version>1.0.0</version>
+                <groupId>io.opentelemetry.instrumentation</groupId>
+                <artifactId>opentelemetry-instrumentation-bom-alpha</artifactId>
+                <version>${version.opentelemetry-instrumentation-bom}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>io.opentelemetry</groupId>
+                <artifactId>opentelemetry-bom</artifactId>
+                <version>1.37.0</version>
+                <type>pom</type>
+                <scope>import</scope>
             </dependency>
             <dependency>
                 <groupId>io.swagger.core.v3</groupId>
index 77e13db..55abffc 100644 (file)
         <minimum-coverage>0.98</minimum-coverage>
     </properties>
     <dependencies>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-exporter-otlp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk-extension-jaeger-remote-sampler</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry.instrumentation</groupId>
+            <artifactId>opentelemetry-kafka-clients-2.6</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <artifactId>spock</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-actuator-autoconfigure</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfig.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfig.java
new file mode 100644 (file)
index 0000000..bcbacbd
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * 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
+ *
+ *       http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.config;
+
+import io.micrometer.observation.ObservationPredicate;
+import io.micrometer.observation.ObservationRegistry;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+import io.opentelemetry.sdk.extension.trace.jaeger.sampler.JaegerRemoteSampler;
+import io.opentelemetry.sdk.trace.samplers.Sampler;
+import java.time.Duration;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryCustomizer;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.server.observation.ServerRequestObservationContext;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+
+@Configuration
+public class OpenTelemetryConfig {
+
+    public static final int JAEGER_REMOTE_SAMPLER_POLLING_INTERVAL_IN_SECOND = 30;
+
+    @Value("${spring.application.name:cps-application}")
+    private String serviceId;
+
+    @Value("${cps.tracing.exporter.endpoint:http://onap-otel-collector:4317}")
+    private String tracingExporterEndpointUrl;
+
+    @Value("${cps.tracing.sampler.jaeger_remote.endpoint:http://onap-otel-collector:14250}")
+    private String jaegerRemoteSamplerUrl;
+
+    /**
+    * OTLP Exporter with Grpc exporter protocol.
+    */
+    @Bean
+    @ConditionalOnExpression(
+        "${cps.tracing.enabled} && 'grpc'.equals('${cps.tracing.exporter.protocol}')")
+    public OtlpGrpcSpanExporter createOtlpExporterGrpc() {
+        return OtlpGrpcSpanExporter.builder().setEndpoint(tracingExporterEndpointUrl).build();
+    }
+
+    /**
+     * OTLP Exporter with HTTP exporter protocol.
+     */
+    @Bean
+    @ConditionalOnExpression(
+        "${cps.tracing.enabled} && 'http'.equals('${cps.tracing.exporter.protocol}')")
+    public OtlpHttpSpanExporter createOtlpExporterHttp() {
+        return OtlpHttpSpanExporter.builder().setEndpoint(tracingExporterEndpointUrl).build();
+    }
+
+    /**
+     * Jaeger Remote Sampler.
+     */
+    @Bean
+    @ConditionalOnProperty("cps.tracing.enabled")
+    public JaegerRemoteSampler createJaegerRemoteSampler() {
+        return JaegerRemoteSampler.builder()
+          .setEndpoint(jaegerRemoteSamplerUrl)
+          .setPollingInterval(Duration.ofSeconds(JAEGER_REMOTE_SAMPLER_POLLING_INTERVAL_IN_SECOND))
+          .setInitialSampler(Sampler.alwaysOff())
+          .setServiceName(serviceId)
+          .build();
+    }
+
+    /**
+   * Excluding /actuator/** endpoints.
+   */
+    @Bean
+    @ConditionalOnProperty("cps.tracing.enabled")
+    ObservationRegistryCustomizer<ObservationRegistry> skipActuatorEndpointsFromObservation() {
+        final PathMatcher pathMatcher = new AntPathMatcher("/");
+        return registry ->
+          registry.observationConfig().observationPredicate(observationPredicate(pathMatcher));
+    }
+
+    /**
+     * Excluding /actuator/** endpoints.
+     */
+    static ObservationPredicate observationPredicate(final PathMatcher pathMatcher) {
+        return (name, context) -> {
+            if (context instanceof ServerRequestObservationContext observationContext) {
+                return !pathMatcher.match("/actuator/**", observationContext.getCarrier().getRequestURI());
+            } else {
+                return true;
+            }
+        };
+    }
+}
\ No newline at end of file
index 167df5a..cf6f1c5 100644 (file)
 package org.onap.cps.ncmp.api.impl.config.kafka;
 
 import io.cloudevents.CloudEvent;
+import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingConsumerInterceptor;
+import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingProducerInterceptor;
 import java.time.Duration;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.producer.ProducerConfig;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
 import org.springframework.boot.ssl.SslBundles;
 import org.springframework.context.annotation.Bean;
@@ -52,6 +56,9 @@ public class KafkaConfig<T> {
 
     private final KafkaProperties kafkaProperties;
 
+    @Value("${cps.tracing.enabled:false}")
+    private boolean tracingEnabled;
+
     private static final SslBundles NO_SSL = null;
 
     /**
@@ -64,6 +71,10 @@ public class KafkaConfig<T> {
     public ProducerFactory<String, T> legacyEventProducerFactory() {
         final Map<String, Object> producerConfigProperties = kafkaProperties.buildProducerProperties(NO_SSL);
         producerConfigProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+        if (tracingEnabled) {
+            producerConfigProperties.put(
+                ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName());
+        }
         return new DefaultKafkaProducerFactory<>(producerConfigProperties);
     }
 
@@ -77,6 +88,10 @@ public class KafkaConfig<T> {
     public ConsumerFactory<String, T> legacyEventConsumerFactory() {
         final Map<String, Object> consumerConfigProperties = kafkaProperties.buildConsumerProperties(NO_SSL);
         consumerConfigProperties.put("spring.deserializer.value.delegate.class", JsonDeserializer.class);
+        if (tracingEnabled) {
+            consumerConfigProperties.put(
+                ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName());
+        }
         return new DefaultKafkaConsumerFactory<>(consumerConfigProperties);
     }
 
@@ -90,6 +105,9 @@ public class KafkaConfig<T> {
     public KafkaTemplate<String, T> legacyEventKafkaTemplate() {
         final KafkaTemplate<String, T> kafkaTemplate = new KafkaTemplate<>(legacyEventProducerFactory());
         kafkaTemplate.setConsumerFactory(legacyEventConsumerFactory());
+        if (tracingEnabled) {
+            kafkaTemplate.setObservationEnabled(true);
+        }
         return kafkaTemplate;
     }
 
@@ -104,6 +122,9 @@ public class KafkaConfig<T> {
                 new ConcurrentKafkaListenerContainerFactory<>();
         containerFactory.setConsumerFactory(legacyEventConsumerFactory());
         containerFactory.getContainerProperties().setAuthExceptionRetryInterval(Duration.ofSeconds(10));
+        if (tracingEnabled) {
+            containerFactory.getContainerProperties().setObservationEnabled(true);
+        }
         return containerFactory;
     }
 
@@ -116,6 +137,10 @@ public class KafkaConfig<T> {
     @Bean
     public ProducerFactory<String, CloudEvent> cloudEventProducerFactory() {
         final Map<String, Object> producerConfigProperties = kafkaProperties.buildProducerProperties(NO_SSL);
+        if (tracingEnabled) {
+            producerConfigProperties.put(
+                ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName());
+        }
         return new DefaultKafkaProducerFactory<>(producerConfigProperties);
     }
 
@@ -128,6 +153,10 @@ public class KafkaConfig<T> {
     @Bean
     public ConsumerFactory<String, CloudEvent> cloudEventConsumerFactory() {
         final Map<String, Object> consumerConfigProperties = kafkaProperties.buildConsumerProperties(NO_SSL);
+        if (tracingEnabled) {
+            consumerConfigProperties.put(
+                ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName());
+        }
         return new DefaultKafkaConsumerFactory<>(consumerConfigProperties);
     }
 
@@ -142,6 +171,9 @@ public class KafkaConfig<T> {
         final KafkaTemplate<String, CloudEvent> kafkaTemplate =
             new KafkaTemplate<>(cloudEventProducerFactory());
         kafkaTemplate.setConsumerFactory(cloudEventConsumerFactory());
+        if (tracingEnabled) {
+            kafkaTemplate.setObservationEnabled(true);
+        }
         return kafkaTemplate;
     }
 
@@ -157,6 +189,9 @@ public class KafkaConfig<T> {
                 new ConcurrentKafkaListenerContainerFactory<>();
         containerFactory.setConsumerFactory(cloudEventConsumerFactory());
         containerFactory.getContainerProperties().setAuthExceptionRetryInterval(Duration.ofSeconds(10));
+        if (tracingEnabled) {
+            containerFactory.getContainerProperties().setObservationEnabled(true);
+        }
         return containerFactory;
     }
 
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfigSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfigSpec.groovy
new file mode 100644 (file)
index 0000000..07395cf
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * 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
+ *
+ *       http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.config
+
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter
+import io.opentelemetry.sdk.extension.trace.jaeger.sampler.JaegerRemoteSampler
+import org.spockframework.spring.SpringBean
+import org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryCustomizer
+import spock.lang.Shared
+import spock.lang.Specification
+
+class OpenTelemetryConfigSpec extends Specification{
+
+    @Shared
+    @SpringBean
+    OpenTelemetryConfig openTelemetryConfig = new OpenTelemetryConfig()
+
+    def setupSpec() {
+        openTelemetryConfig.tracingExporterEndpointUrl="http://tracingExporterEndpointUrl"
+        openTelemetryConfig.jaegerRemoteSamplerUrl="http://jaegerremotesamplerurl"
+        openTelemetryConfig.serviceId ="cps-application"
+    }
+
+    def 'OpenTelemetryConfig Construction.'() {
+        expect: 'the system can create an instance'
+        new OpenTelemetryConfig() != null
+    }
+
+    def  'OTLP Exporter creation with Grpc protocol'(){
+        when: 'an OTLP exporter is created'
+            def result = openTelemetryConfig.createOtlpExporterGrpc()
+        then: 'an OTLP Exporter is created'
+            assert result instanceof OtlpGrpcSpanExporter
+    }
+
+    def  'OTLP Exporter creation with HTTP protocol'(){
+        when: 'an OTLP exporter is created'
+            def result = openTelemetryConfig.createOtlpExporterHttp()
+        then: 'an OTLP Exporter is created'
+            assert result instanceof OtlpHttpSpanExporter
+        and:
+            assert result.builder.endpoint=="http://tracingExporterEndpointUrl"
+    }
+
+    def  'Jaeger Remote Sampler Creation'(){
+        when: 'an OTLP exporter is created'
+            def result = openTelemetryConfig.createJaegerRemoteSampler()
+        then: 'an OTLP Exporter is created'
+            assert result instanceof JaegerRemoteSampler
+        and:
+            assert result.delegate.type=="remoteSampling"
+        and:
+            assert result.delegate.url.toString().startsWith("http://jaegerremotesamplerurl")
+    }
+
+    def  'Skipping Acutator endpoints'(){
+        when: 'an OTLP exporter is created'
+            def result = openTelemetryConfig.skipActuatorEndpointsFromObservation()
+        then: 'an OTLP Exporter is created'
+            assert result instanceof ObservationRegistryCustomizer
+    }
+}
index 16f27d0..4d3fd66 100644 (file)
@@ -18,7 +18,7 @@
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.api.impl.config.kafka;
+package org.onap.cps.ncmp.api.impl.config.kafka
 
 import io.cloudevents.CloudEvent
 import io.cloudevents.kafka.CloudEventDeserializer
@@ -31,12 +31,14 @@ import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.kafka.core.KafkaTemplate
 import org.springframework.kafka.support.serializer.JsonDeserializer
 import org.springframework.kafka.support.serializer.JsonSerializer
+import org.springframework.test.context.TestPropertySource
 import spock.lang.Shared
 import spock.lang.Specification
 
 @SpringBootTest(classes = [KafkaProperties, KafkaConfig])
 @EnableSharedInjection
 @EnableConfigurationProperties
+@TestPropertySource(properties = ["cps.tracing.enabled=true"])
 class KafkaConfigSpec extends Specification {
 
     @Shared
index a4b9ea9..58e6287 100644 (file)
@@ -219,3 +219,9 @@ hazelcast:
     kubernetes:
       enabled: false
       service-name: cps-and-ncmp-service
+
+cps:
+  tracing:
+    enabled: false
+    exporter:
+      protocol: grpc