diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml new file mode 100644 index 0000000..24c198d --- /dev/null +++ b/.github/workflows/acceptance-tests.yml @@ -0,0 +1,50 @@ +name: Acceptance Tests + +on: + pull_request: + branches: + - main + types: + - labeled + - opened + - synchronize + - reopened + +jobs: + acceptance-tests: + # see https://stackoverflow.com/questions/62325286/run-github-actions-when-pull-requests-have-a-specific-label + if: contains(github.event.pull_request.labels.*.name, 'oats') + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + - name: Check out oats + uses: actions/checkout@v4 + with: + repository: grafana/oats + ref: d2e59f9857d898f9d0f606714de3b22ee9d61804 + path: oats + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + cache-dependency-path: oats/go.sum + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1.1.0 + - name: Setup Gradle and run build + uses: gradle/gradle-build-action@v2.10.0 + working-directory: integrationTests/oats + run: ./gradlew build + - name: Run acceptance tests + run: ./scripts/run-acceptance-tests.sh + - name: upload log file + uses: actions/upload-artifact@v3 + if: failure() + with: + name: docker-compose.log + path: oats/yaml/build/**/output.log diff --git a/CHANGELOG.md b/CHANGELOG.md index fe6328d..8ad9cc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * gRPC not supported anymore * Environment variables starting with "OTEL_" are not supported anymore, because they will only affect the trace and log exporter * Application name and version are not read from MANIFEST.MF anymore, because this also wrongly reads information from dependencies +* support OTEL_EXPORTER_OTLP_ENDPOINT in addition to grafana.otlp.onprem.endpoint ## Version 1.4.0 (2023-12-07) diff --git a/integrationTests/log4j/src/test/java/com/grafana/opentelemetry/log4j/Log4jIntegrationTest.java b/integrationTests/log4j/src/test/java/com/grafana/opentelemetry/log4j/Log4jIntegrationTest.java index 5d99d0f..603b5a5 100644 --- a/integrationTests/log4j/src/test/java/com/grafana/opentelemetry/log4j/Log4jIntegrationTest.java +++ b/integrationTests/log4j/src/test/java/com/grafana/opentelemetry/log4j/Log4jIntegrationTest.java @@ -23,7 +23,6 @@ @TestPropertySource( properties = { "grafana.otlp.onprem.endpoint = http://localhost:${mockServerPort}", - "grafana.otlp.onprem.protocol = http/protobuf", }) public class Log4jIntegrationTest { diff --git a/integrationTests/main/src/test/java/com/grafana/opentelemetry/IntegrationTest.java b/integrationTests/main/src/test/java/com/grafana/opentelemetry/IntegrationTest.java index b1ee412..d83e082 100644 --- a/integrationTests/main/src/test/java/com/grafana/opentelemetry/IntegrationTest.java +++ b/integrationTests/main/src/test/java/com/grafana/opentelemetry/IntegrationTest.java @@ -1,8 +1,6 @@ package com.grafana.opentelemetry; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; @@ -17,13 +15,11 @@ import org.apache.commons.lang3.reflect.MethodUtils; import org.junit.jupiter.api.Test; import org.mockserver.client.MockServerClient; -import org.mockserver.model.HttpRequest; import org.mockserver.springtest.MockServerTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpMethod; import org.springframework.test.context.TestPropertySource; @SpringBootTest( @@ -44,6 +40,10 @@ class IntegrationTest { @Autowired private GrafanaProperties properties; + @SuppressWarnings("unused") + @Autowired + private ConnectionProperties connectionProperties; + @Autowired private Optional sdk; @Test @@ -90,30 +90,4 @@ private static List> getMetricsResourceAttributes() } return Collections.emptyList(); } - - @Test - void dataIsSent() { - restTemplate.getForEntity("/hello", String.class); - - await() - .atMost(10, SECONDS) - .untilAsserted( - () -> { - verifyPath("/v1/traces"); - verifyPath("/v1/metrics"); - verifyPath("/v1/logs"); - }); - } - - private void verifyPath(String path) { - // only assert that a request was received, - // because the goal of this test is to make sure that data is still sent when dependabot - // upgrades - // spring boot, which can also update the OpenTelemetry version - mockServerClient.verify( - HttpRequest.request() - .withMethod(HttpMethod.POST.name()) - .withPath(path) - .withHeader("Content-Type", "application/x-protobuf")); - } } diff --git a/integrationTests/oats/build.gradle b/integrationTests/oats/build.gradle new file mode 100644 index 0000000..d7d1a27 --- /dev/null +++ b/integrationTests/oats/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.0' + id 'io.spring.dependency-management' version '1.1.4' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation rootProject +} diff --git a/integrationTests/oats/docker-compose.yaml b/integrationTests/oats/docker-compose.yaml new file mode 100644 index 0000000..7993901 --- /dev/null +++ b/integrationTests/oats/docker-compose.yaml @@ -0,0 +1,17 @@ +version: '3.9' + +services: + application: + image: eclipse-temurin:17-jre + volumes: + - ./build/libs:/libs + environment: + OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector:4318" + OTEL_METRIC_EXPORT_INTERVAL: "5000" # so we don't have to wait 60s for metrics + command: + - /bin/bash + - -c + - java -jar /libs/*.jar + ports: + - 8080:8080 + diff --git a/integrationTests/oats/jvm-dashboard.json b/integrationTests/oats/jvm-dashboard.json new file mode 100644 index 0000000..c4f4c7a --- /dev/null +++ b/integrationTests/oats/jvm-dashboard.json @@ -0,0 +1,945 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Dashboard for JVM metrics with OpenTelemetry instrumentation", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 18812, + "graphTooltip": 0, + "links": [ + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": false, + "title": "Semantic Conventions: 1.20.0 - 1.22.0", + "tooltip": "multiple versions of the semantic conventions are supported using 'or' and regex queries", + "type": "link", + "url": "https://github.com/open-telemetry/semantic-conventions/blob/main/schemas/1.20.0" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "HTTP server request rate", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 51, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by (instance) (rate({__name__=~\"http_server_request_duration_seconds_count|http_server_request_duration_count|http_server_duration_seconds_count|http_server_duration_count|http_server_duration_milliseconds_count|http_server_requests_milliseconds_count|http_server_requests_count|http_server_requests_seconds_count\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "HTTP server error ratio - ratio of requests that return 5xx", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 4, + "y": 0 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(sum by (instance)(rate({__name__=~\"http_server_request_duration_seconds_count|http_server_request_duration_count\", job=~\"$job\", instance=~\"$instance\", http_response_status_code=~\"5.*\"}[$__rate_interval]))) / on (instance) (sum by (instance)(rate({__name__=~\"http_server_request_duration_seconds_count|http_server_request_duration_count\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])))\nor\n(sum by (instance)(rate({__name__=~\"http_server_duration_milliseconds_count|http_server_duration_seconds_count|http_server_duration_count\", job=~\"$job\", instance=~\"$instance\", http_status_code=~\"5.*\"}[$__rate_interval]))) / on (instance) (sum by (instance)(rate({__name__=~\"http_server_duration_milliseconds_count|http_server_duration_seconds_count|http_server_duration_count\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])))\nor\n(sum by (instance)(rate({__name__=~\"http_server_requests_milliseconds_count|http_server_requests_count|http_server_requests_seconds_count\", job=~\"$job\", instance=~\"$instance\", outcome=\"SERVER_ERROR\"}[$__rate_interval]))) / on (instance) (sum by (instance)(rate({__name__=~\"http_server_requests_milliseconds_count|http_server_requests_count|http_server_requests_seconds_count\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])))\n", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Error %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "95th percentile of HTTP server request duration in seconds", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 9, + "y": 0 + }, + "id": 52, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(histogram_quantile(0.95, sum by(le, instance) (rate({__name__=~\"http_server_request_duration_seconds_bucket|http_server_request_duration_bucket|http_server_duration_seconds_bucket|http_server_duration_bucket|http_server_requests_seconds_bucket\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))))\nor\n(histogram_quantile(0.95, sum by(le, instance) (rate({__name__=~\"http_server_duration_milliseconds_bucket|http_server_requests_milliseconds_bucket|http_server_requests_bucket\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))) / 1000)\n", + "instant": false, + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Duration (95%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "CPU utilization for the whole system", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 7, + "x": 0, + "y": 8 + }, + "id": 38, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "{__name__=~\"jvm_cpu_recent_utilization|jvm_cpu_recent_utilization_ratio|process_runtime_jvm_system_cpu_utilization|process_runtime_jvm_system_cpu_utilization_ratio|system_cpu_usage\", job=~\"$job\", instance=~\"$instance\"}", + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "CPU utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Used heap memory / heap memory limit ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 7, + "x": 7, + "y": 8 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(sum by (instance) ({__name__=~\"jvm_memory_used|jvm_memory_used_bytes\",job=~\"$job\",jvm_memory_type=\"heap\", instance=~\"$instance\"}) / on(instance) sum by (instance) ({__name__=~\"jvm_memory_limit|jvm_memory_limit_bytes\",job=~\"$job\",jvm_memory_type=\"heap\", instance=~\"$instance\"}))\nor\n(sum by (instance) ({__name__=~\"process_runtime_jvm_memory_usage|process_runtime_jvm_memory_usage_bytes\",job=~\"$job\",type=\"heap\", instance=~\"$instance\"}) / on(instance) sum by (instance) ({__name__=~\"process_runtime_jvm_memory_limit|process_runtime_jvm_memory_limit_bytes\",job=~\"$job\",type=\"heap\", instance=~\"$instance\"}))\nor\n(sum by (instance) ({__name__=~\"jvm_memory_used|jvm_memory_used_bytes\",job=~\"$job\",area=\"heap\", instance=~\"$instance\"}) / on(instance) sum by (instance) ({__name__=~\"jvm_memory_max|jvm_memory_max_bytes\",job=~\"$job\",area=\"heap\", instance=~\"$instance\"}))", + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Heap Memory Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of time spend for garbage collection pauses", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 15 + }, + "id": 46, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "(sum by(instance) (rate({__name__=~\"jvm_gc_duration_sum|jvm_gc_duration_seconds_sum|process_runtime_jvm_gc_duration_sum|process_runtime_jvm_gc_duration_seconds_sum|jvm_gc_pause_sum|jvm_gc_pause_seconds_sum\",job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])))\nor\n(sum by(instance) (rate(jvm_gc_pause_milliseconds_sum{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) / 1000)", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Garbage Collection", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Number of currently loaded classes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 4, + "y": 15 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "{__name__=~\"jvm_class_count|process_runtime_jvm_classes_current_loaded|jvm_classes_loaded\",job=~\"$job\", instance=~\"$instance\"}", + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Classes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Number of currently executing (also called \"live\") threads", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 9, + "y": 15 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum({__name__=~\"jvm_thread_count|process_runtime_jvm_threads_count|jvm_threads_live\",job=~\"$job\", instance=~\"$instance\"}) by (instance)", + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Threads", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [ + "JVM", + "open-telemetry", + "Java", + "otel", + "opentelemetry", + "otlp" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "description": "Choose a Prometheus data source", + "hide": 0, + "includeAll": false, + "label": "Data source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".+", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values({__name__=~\"jvm_class_count|process_runtime_jvm_classes_current_loaded|jvm_classes_loaded\"},job)", + "hide": 0, + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values({__name__=~\"jvm_class_count|process_runtime_jvm_classes_current_loaded|jvm_classes_loaded\"},job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values({__name__=~\"(jvm_class_count|process_runtime_jvm_classes_current_loaded|jvm_classes_loaded)\", job=~\"$job\"},instance)", + "description": "The instance of the application, e.g. pod1", + "hide": 0, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values({__name__=~\"(jvm_class_count|process_runtime_jvm_classes_current_loaded|jvm_classes_loaded)\", job=~\"$job\"},instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "JVM Overview (OpenTelemetry)", + "uid": "b91844d7-121e-4d0a-93b8-a9c1a05703b3", + "version": 1, + "weekStart": "" +} diff --git a/integrationTests/oats/oats.yaml b/integrationTests/oats/oats.yaml new file mode 100644 index 0000000..eb3fcce --- /dev/null +++ b/integrationTests/oats/oats.yaml @@ -0,0 +1,35 @@ +docker-compose: + generator: lgtm + files: + - docker-compose.yaml +input: + - path: /hello +expected: + logs: + - logql: '{exporter = "OTLP"}' + contains: + - 'hello LGTM' + traces: + - traceql: '{ name = "http get /hello" }' + spans: + - name: 'http get /hello' + dashboards: + - path: jvm-dashboard.json + # rate_interval: 2m + panels: + - title: Rate + value: "> 0" + - title: Error % + value: "> 0" + - title: Duration (95%) + value: "> 0" + - title: CPU utilization + value: ">= 0" + - title: Heap Memory Utilization + value: "> 0" + - title: Garbage Collection + value: ">= 0" + - title: Classes + value: "> 0" + - title: Threads + value: "> 0" diff --git a/integrationTests/oats/src/main/java/com/grafana/opentelemetry/DemoApplication.java b/integrationTests/oats/src/main/java/com/grafana/opentelemetry/DemoApplication.java new file mode 100644 index 0000000..d040957 --- /dev/null +++ b/integrationTests/oats/src/main/java/com/grafana/opentelemetry/DemoApplication.java @@ -0,0 +1,11 @@ +package com.grafana.opentelemetry; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/integrationTests/oats/src/main/java/com/grafana/opentelemetry/HelloController.java b/integrationTests/oats/src/main/java/com/grafana/opentelemetry/HelloController.java new file mode 100644 index 0000000..cfaf6ab --- /dev/null +++ b/integrationTests/oats/src/main/java/com/grafana/opentelemetry/HelloController.java @@ -0,0 +1,24 @@ +package com.grafana.opentelemetry; + +import java.util.Random; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + private static final Logger LOG = LoggerFactory.getLogger(HelloController.class); + + private final Random random = new Random(); + + @GetMapping("/hello") + public String sayHello() { + LOG.info("hello LGTM"); + if (random.nextBoolean()) { + throw new RuntimeException("Failed to get cart"); + } + return "hello LGTM"; + } +} diff --git a/scripts/run-acceptance-tests.sh b/scripts/run-acceptance-tests.sh new file mode 100755 index 0000000..a8e07cc --- /dev/null +++ b/scripts/run-acceptance-tests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd oats/yaml +go install github.com/onsi/ginkgo/v2/ginkgo +export TESTCASE_SKIP_BUILD=true +export TESTCASE_TIMEOUT=5m +export TESTCASE_BASE_PATH=../../integrationTests +ginkgo -r # is parallel causing problems? -p diff --git a/settings.gradle b/settings.gradle index ae4936d..8247d6b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,5 +3,6 @@ rootProject.name = 'grafana-opentelemetry-starter' include( ":integrationTests:main", ":integrationTests:disable", - ":integrationTests:log4j" + ":integrationTests:log4j", + ":integrationTests:oats" ) diff --git a/src/main/java/com/grafana/opentelemetry/ConnectionProperties.java b/src/main/java/com/grafana/opentelemetry/ConnectionProperties.java index d44c118..b65c0b0 100644 --- a/src/main/java/com/grafana/opentelemetry/ConnectionProperties.java +++ b/src/main/java/com/grafana/opentelemetry/ConnectionProperties.java @@ -1,22 +1,7 @@ package com.grafana.opentelemetry; +import java.time.Duration; import java.util.Map; import java.util.Optional; -class ConnectionProperties { - private final Map headers; - private final Optional endpoint; - - public ConnectionProperties(Optional endpoint, Map headers) { - this.headers = headers; - this.endpoint = endpoint; - } - - public Map getHeaders() { - return headers; - } - - public Optional getEndpoint() { - return endpoint; - } -} +public record ConnectionProperties(Optional endpoint, Map headers, Duration metricExportInterval) {} diff --git a/src/main/java/com/grafana/opentelemetry/DistributionVersion.java b/src/main/java/com/grafana/opentelemetry/DistributionVersion.java index 243574a..d0390de 100644 --- a/src/main/java/com/grafana/opentelemetry/DistributionVersion.java +++ b/src/main/java/com/grafana/opentelemetry/DistributionVersion.java @@ -9,5 +9,5 @@ public class DistributionVersion { - public static final String VERSION = "3.2.0"; + public static final String VERSION = "3.2.0-beta.1"; } diff --git a/src/main/java/com/grafana/opentelemetry/MetricsOtlpConfig.java b/src/main/java/com/grafana/opentelemetry/MetricsOtlpConfig.java index 22a6c97..57c4c61 100644 --- a/src/main/java/com/grafana/opentelemetry/MetricsOtlpConfig.java +++ b/src/main/java/com/grafana/opentelemetry/MetricsOtlpConfig.java @@ -1,6 +1,9 @@ package com.grafana.opentelemetry; +import io.micrometer.core.instrument.config.validate.DurationValidator; import io.micrometer.registry.otlp.OtlpConfig; + +import java.time.Duration; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -23,14 +26,14 @@ public Map resourceAttributes() { @Override public String url() { return connectionProperties - .getEndpoint() + .endpoint() .map(s -> s + "/v1/metrics") .orElse(OtlpConfig.DEFAULT.url()); } @Override public Map headers() { - return connectionProperties.getHeaders(); + return connectionProperties.headers(); } @Override @@ -42,4 +45,9 @@ public String get(String key) { public TimeUnit baseTimeUnit() { return TimeUnit.SECONDS; } + + @Override + public Duration step() { + return connectionProperties.metricExportInterval(); + } } diff --git a/src/main/java/com/grafana/opentelemetry/OpenTelemetryConfig.java b/src/main/java/com/grafana/opentelemetry/OpenTelemetryConfig.java index 7403ef9..496aba7 100644 --- a/src/main/java/com/grafana/opentelemetry/OpenTelemetryConfig.java +++ b/src/main/java/com/grafana/opentelemetry/OpenTelemetryConfig.java @@ -5,6 +5,10 @@ import io.micrometer.common.util.StringUtils; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.config.validate.DurationValidator; +import io.micrometer.core.instrument.config.validate.InvalidReason; +import io.micrometer.core.instrument.config.validate.PropertyValidator; +import io.micrometer.core.instrument.config.validate.Validated; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.opentelemetry.api.OpenTelemetry; @@ -15,6 +19,8 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.semconv.ResourceAttributes; import java.lang.reflect.Method; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Collections; import java.util.HashMap; @@ -23,6 +29,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.logging.log4j.util.Strings; @@ -62,11 +69,13 @@ public Resource getResource(AutoConfiguredOpenTelemetrySdk sdk) { @Bean public OtlpMeterRegistry openTelemetryMeterRegistry( - Clock clock, GrafanaProperties properties, Resource resource) { + Clock clock, + GrafanaProperties properties, + Resource resource, + ConnectionProperties connectionProperties) { return new OtlpMeterRegistry( new MetricsOtlpConfig( - getMap(resource, k -> !EXCLUDED_ATTRIBUTES.contains(k)), - connectionProperties(properties)), + getMap(resource, k -> !EXCLUDED_ATTRIBUTES.contains(k)), connectionProperties), clock); } @@ -109,10 +118,12 @@ static void tryAddAppender( @Bean public AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk( GrafanaProperties properties, + ConnectionProperties connectionProperties, @Value("${spring.application.name:#{null}}") String applicationName) { - AutoConfiguredOpenTelemetrySdkBuilder builder = AutoConfiguredOpenTelemetrySdk.builder(); + // the log record exporter uses the global instance, so we need to set it as global to avoid a warning + AutoConfiguredOpenTelemetrySdkBuilder builder = AutoConfiguredOpenTelemetrySdk.builder().setResultAsGlobal(); - Map configProperties = getConfigProperties(properties); + Map configProperties = getConfigProperties(properties, connectionProperties); builder.addPropertiesSupplier(() -> configProperties); builder.addResourceCustomizer( (resource, unused) -> { @@ -131,34 +142,43 @@ public AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk( } } - private static Map getConfigProperties(GrafanaProperties properties) { + private Map getConfigProperties( + GrafanaProperties properties, ConnectionProperties connectionProperties) { String exporters = properties.isDebugLogging() ? "logging,otlp" : "otlp"; - ConnectionProperties p = connectionProperties(properties); Map configProperties = new HashMap<>( Map.of( "otel.exporter.otlp.protocol", PROTOCOL, "otel.traces.exporter", exporters, "otel.logs.exporter", exporters)); - if (!p.getHeaders().isEmpty()) { + if (!connectionProperties.headers().isEmpty()) { configProperties.put( OTLP_HEADERS, - p.getHeaders().entrySet().stream() + connectionProperties.headers().entrySet().stream() .map(e -> String.format("%s=%s", e.getKey(), e.getValue())) .collect(Collectors.joining(","))); } - p.getEndpoint().ifPresent(s -> configProperties.put("otel.exporter.otlp.endpoint", s)); + connectionProperties + .endpoint() + .ifPresent(s -> configProperties.put("otel.exporter.otlp.endpoint", s)); return configProperties; } - private static ConnectionProperties connectionProperties(GrafanaProperties properties) { + @Bean + public ConnectionProperties connectionProperties( + GrafanaProperties properties, + @Value("${otel.exporter.otlp.endpoint:#{null}}") String otlpEndpoint, + @Value("${otel.metric.export.interval:60000}") String metricExportInterval + ) { GrafanaProperties.CloudProperties cloud = properties.getCloud(); Map headers = getHeaders(cloud.getInstanceId(), cloud.getApiKey()); - Optional endpoint = - getEndpoint(properties.getOnPrem().getEndpoint(), cloud.getZone(), headers); + if (StringUtils.isBlank(otlpEndpoint)) { + otlpEndpoint = properties.getOnPrem().getEndpoint(); + } + Optional endpoint = getEndpoint(otlpEndpoint, cloud.getZone(), headers); - return new ConnectionProperties(endpoint, headers); + return new ConnectionProperties(endpoint, headers, Duration.of(Integer.parseInt(metricExportInterval), ChronoUnit.MILLIS)); } static Map maskAuthHeader(Map configProperties) {