Skip to content

Commit

Permalink
Add support for OTEL metrics export (#339)
Browse files Browse the repository at this point in the history
Allows export of the internal OTEL metrics
  • Loading branch information
matt-richardson authored Nov 28, 2024
1 parent 411061d commit 5490846
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -59,18 +64,35 @@ public SpanProcessor buildSpanProcessor(String endpoint, Map<String, String> 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<String, String> headers, String exporterEndpoint) {
@Nullable
private MetricExporter buildMetricsExporter(String endpoint, Map<String, String> 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<String, String> 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();
}
Expand All @@ -85,6 +107,7 @@ public void mapParamsToModel(Map<String, String> params, Map<String, Object> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}

Expand All @@ -39,6 +41,7 @@ protected void serviceSpecificValidate(ActionErrors errors) {
protected void mapServiceSpecificParams(HashMap<String, String> params, ArrayList<HeaderDto> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@
<span class="error" id="error_honeycombDataset"></span>
</td>
</tr>
<tr <c:if test='${otelService != "honeycomb.io"}'>style="display: none"</c:if>>
<th><label for="honeycombMetricsEnabled">Send Metrics?</label></th>
<td>
<forms:checkbox name="honeycombMetricsEnabled" checked="${otelHoneycombMetricsEnabled}" >&nbsp;</forms:checkbox>
</td>
</tr>

0 comments on commit 5490846

Please sign in to comment.