From 5490846ae1065ce103950e927514b402ee0b64cc Mon Sep 17 00:00:00 2001 From: Matt Richardson Date: Thu, 28 Nov 2024 13:25:39 +1100 Subject: [PATCH] Add support for OTEL metrics export (#339) Allows export of the internal OTEL metrics --- .../opentelemetry/common/PluginConstants.java | 2 + server/build.gradle | 1 + .../HoneycombOTELEndpointHandler.java | 49 ++++++++++++++----- ...mbProjectConfigurationSettingsRequest.java | 3 ++ .../server/helpers/OTELMetrics.java | 48 ++++++++++++++++++ ...projectConfigurationSettingsHoneycomb.jspf | 6 +++ 6 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 server/src/main/java/com/octopus/teamcity/opentelemetry/server/helpers/OTELMetrics.java diff --git a/common/src/main/java/com/octopus/teamcity/opentelemetry/common/PluginConstants.java b/common/src/main/java/com/octopus/teamcity/opentelemetry/common/PluginConstants.java index 7a608eb..4aae058 100644 --- a/common/src/main/java/com/octopus/teamcity/opentelemetry/common/PluginConstants.java +++ b/common/src/main/java/com/octopus/teamcity/opentelemetry/common/PluginConstants.java @@ -13,6 +13,8 @@ private PluginConstants() {} public static final String PROPERTY_KEY_HONEYCOMB_TEAM = "octopus.teamcity.opentelemetry.plugin.honeycomb.team"; public static final String PROPERTY_KEY_HONEYCOMB_DATASET = "octopus.teamcity.opentelemetry.plugin.honeycomb.dataset"; public static final String PROPERTY_KEY_HONEYCOMB_APIKEY = "octopus.teamcity.opentelemetry.plugin.honeycomb.apikey"; + public static final String PROPERTY_KEY_HONEYCOMB_METRICS_ENABLED = "octopus.teamcity.opentelemetry.plugin.honeycomb.metrics.enabled"; + public static final String ATTRIBUTE_SERVICE_NAME = "service_name"; public static final String ATTRIBUTE_NAME = "name"; diff --git a/server/build.gradle b/server/build.gradle index 9ad0610..290eab2 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -18,6 +18,7 @@ dependencies { implementation('io.opentelemetry:opentelemetry-api:1.44.1') implementation('io.opentelemetry:opentelemetry-sdk:1.44.1') implementation('io.opentelemetry:opentelemetry-exporter-otlp:1.44.1') + implementation 'io.opentelemetry:opentelemetry-sdk-metrics:1.28.0' implementation('io.opentelemetry:opentelemetry-semconv:1.31.0-alpha-SNAPSHOT') implementation('io.opentelemetry:opentelemetry-exporter-zipkin') implementation 'io.grpc:grpc-netty-shaded:1.68.1' diff --git a/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/HoneycombOTELEndpointHandler.java b/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/HoneycombOTELEndpointHandler.java index 7e998a8..9dc77b1 100644 --- a/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/HoneycombOTELEndpointHandler.java +++ b/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/HoneycombOTELEndpointHandler.java @@ -1,12 +1,17 @@ package com.octopus.teamcity.opentelemetry.server.endpoints.honeycomb; +import com.octopus.teamcity.opentelemetry.common.PluginConstants; import com.octopus.teamcity.opentelemetry.server.*; import com.octopus.teamcity.opentelemetry.server.endpoints.IOTELEndpointHandler; +import com.octopus.teamcity.opentelemetry.server.helpers.OTELMetrics; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import jetbrains.buildServer.serverSide.SBuild; import jetbrains.buildServer.serverSide.crypt.EncryptUtil; import jetbrains.buildServer.serverSide.crypt.RSACipher; @@ -15,8 +20,9 @@ import org.jetbrains.annotations.NotNull; import org.springframework.web.servlet.ModelAndView; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; +import java.time.Duration; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -28,8 +34,7 @@ public class HoneycombOTELEndpointHandler implements IOTELEndpointHandler { private final PluginDescriptor pluginDescriptor; static Logger LOG = Logger.getLogger(HoneycombOTELEndpointHandler.class.getName()); - public HoneycombOTELEndpointHandler( - PluginDescriptor pluginDescriptor) { + public HoneycombOTELEndpointHandler(PluginDescriptor pluginDescriptor) { this.pluginDescriptor = pluginDescriptor; } @@ -59,18 +64,35 @@ public SpanProcessor buildSpanProcessor(String endpoint, Map par //todo: add a setting to say "use classic" or "use environments" headers.put("x-honeycomb-dataset", params.get(PROPERTY_KEY_HONEYCOMB_DATASET)); headers.put("x-honeycomb-team", EncryptUtil.unscramble(params.get(PROPERTY_KEY_HONEYCOMB_APIKEY))); - return buildGrpcSpanProcessor(headers, endpoint); + + var metricsExporter = buildMetricsExporter(endpoint, params); + + return buildGrpcSpanProcessor(headers, endpoint, metricsExporter); } - private SpanProcessor buildGrpcSpanProcessor(Map headers, String exporterEndpoint) { + @Nullable + private MetricExporter buildMetricsExporter(String endpoint, Map params) { + if (params.getOrDefault(PROPERTY_KEY_HONEYCOMB_METRICS_ENABLED, "false").equals("true")) { + return OtlpGrpcMetricExporter.builder() + .setEndpoint(endpoint) + .addHeader("x-honeycomb-team", EncryptUtil.unscramble(params.get(PROPERTY_KEY_HONEYCOMB_APIKEY))) + .addHeader("x-honeycomb-dataset", params.get(PROPERTY_KEY_HONEYCOMB_DATASET)) + .build(); + } + return null; + } - OtlpGrpcSpanExporterBuilder spanExporterBuilder = OtlpGrpcSpanExporter.builder(); - headers.forEach(spanExporterBuilder::addHeader); - spanExporterBuilder.setEndpoint(exporterEndpoint); - SpanExporter spanExporter = spanExporterBuilder.build(); + private SpanProcessor buildGrpcSpanProcessor(Map headers, String exporterEndpoint, @Nullable MetricExporter metricsExporter) { + + var serviceNameResource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, PluginConstants.SERVICE_NAME)); + var meterProvider = OTELMetrics.getOTELMeterProvider(metricsExporter, serviceNameResource); - LOG.debug("OTEL_PLUGIN: Opentelemetry export headers: " + LogMasker.mask(headers.toString())); - LOG.debug("OTEL_PLUGIN: Opentelemetry export endpoint: " + exporterEndpoint); + var spanExporterBuilder = OtlpGrpcSpanExporter.builder(); + headers.forEach(spanExporterBuilder::addHeader); + var spanExporter = spanExporterBuilder + .setEndpoint(exporterEndpoint) + .setMeterProvider(meterProvider) + .build(); return BatchSpanProcessor.builder(spanExporter).build(); } @@ -85,6 +107,7 @@ public void mapParamsToModel(Map params, Map mod model.put("otelEndpoint", params.get(PROPERTY_KEY_ENDPOINT)); model.put("otelHoneycombTeam", params.get(PROPERTY_KEY_HONEYCOMB_TEAM)); model.put("otelHoneycombDataset", params.get(PROPERTY_KEY_HONEYCOMB_DATASET)); + model.put("otelHoneycombMetricsEnabled", params.get(PROPERTY_KEY_HONEYCOMB_METRICS_ENABLED)); if (params.get(PROPERTY_KEY_HONEYCOMB_APIKEY) == null) { model.put("otelHoneycombApiKey", null); } diff --git a/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/SetHoneycombProjectConfigurationSettingsRequest.java b/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/SetHoneycombProjectConfigurationSettingsRequest.java index 5398e95..b20f6de 100644 --- a/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/SetHoneycombProjectConfigurationSettingsRequest.java +++ b/server/src/main/java/com/octopus/teamcity/opentelemetry/server/endpoints/honeycomb/SetHoneycombProjectConfigurationSettingsRequest.java @@ -17,11 +17,13 @@ public class SetHoneycombProjectConfigurationSettingsRequest extends SetProjectC public final String honeycombTeam; public final String honeycombDataset; public final String honeycombApiKey; + public final String honeycombMetricsEnabled; public SetHoneycombProjectConfigurationSettingsRequest(HttpServletRequest request) { super(request); this.honeycombTeam = request.getParameter("honeycombTeam"); this.honeycombDataset = request.getParameter("honeycombDataset"); + this.honeycombMetricsEnabled = request.getParameter("honeycombMetricsEnabled"); this.honeycombApiKey = RSACipher.decryptWebRequestData(request.getParameter("encryptedHoneycombApiKey")); } @@ -39,6 +41,7 @@ protected void serviceSpecificValidate(ActionErrors errors) { protected void mapServiceSpecificParams(HashMap params, ArrayList headers) { params.put(PROPERTY_KEY_HONEYCOMB_DATASET, honeycombDataset); params.put(PROPERTY_KEY_HONEYCOMB_TEAM, honeycombTeam); + params.put(PROPERTY_KEY_HONEYCOMB_METRICS_ENABLED, honeycombMetricsEnabled); params.put(PROPERTY_KEY_HONEYCOMB_APIKEY, EncryptUtil.scramble(honeycombApiKey)); } } diff --git a/server/src/main/java/com/octopus/teamcity/opentelemetry/server/helpers/OTELMetrics.java b/server/src/main/java/com/octopus/teamcity/opentelemetry/server/helpers/OTELMetrics.java new file mode 100644 index 0000000..8c4234d --- /dev/null +++ b/server/src/main/java/com/octopus/teamcity/opentelemetry/server/helpers/OTELMetrics.java @@ -0,0 +1,48 @@ +package com.octopus.teamcity.opentelemetry.server.helpers; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; + +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; + +public class OTELMetrics { + + private static final AtomicBoolean metricsConfigured = new AtomicBoolean(false); + private static SdkMeterProvider sdkMeterProvider; + + // this is not quite right... + // it's using project level settings (which can be different across projects) + // but setting up a global configuration. + // likely need to refactor the config so that it has metrics configuration as global + + public static SdkMeterProvider getOTELMeterProvider(@Nullable MetricExporter metricExporter, Resource serviceNameResource) { + if (metricsConfigured.get()) return sdkMeterProvider; + metricsConfigured.set(true); + + var meterProviderBuilder = SdkMeterProvider.builder() + .setResource(Resource.getDefault().merge(serviceNameResource)); + if (metricExporter != null) { + var providedMetricExporterBuilder = PeriodicMetricReader.builder(metricExporter) + .setInterval(Duration.ofSeconds(10)) + .build(); + meterProviderBuilder + .registerMetricReader(providedMetricExporterBuilder); + } + sdkMeterProvider = meterProviderBuilder.build(); + var globalOpenTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .build(); + + GlobalOpenTelemetry.set(globalOpenTelemetry); + + // Shutdown hooks to close resources properly + Runtime.getRuntime().addShutdownHook(new Thread(sdkMeterProvider::close)); + return sdkMeterProvider; + } +} diff --git a/server/src/main/resources/buildServerResources/projectConfigurationSettingsHoneycomb.jspf b/server/src/main/resources/buildServerResources/projectConfigurationSettingsHoneycomb.jspf index d79a488..b990f99 100644 --- a/server/src/main/resources/buildServerResources/projectConfigurationSettingsHoneycomb.jspf +++ b/server/src/main/resources/buildServerResources/projectConfigurationSettingsHoneycomb.jspf @@ -19,3 +19,9 @@ +style="display: none"> + + +   + +