diff --git a/README.md b/README.md index a334b2397..8fec4e90f 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,23 @@ In addition, if the backends were configured then there will be an environment v | ci.pipeline.parameter.name | Name of the parameter | String | | ci.pipeline.parameter.value | Value of the parameter. "Sensitive" values are redacted | String | +Cloud specific: + +| Attribute | Description | Type | +|----------------------------------|--------------|------| +| ci.cloud.label | Labels attached to the agent | String | +| ci.cloud.name | The agent name | String | +| cloud.account.id | The cloud account ID the resource is assigned to | String | +| cloud.provider | Name of the cloud provider | String | +| cloud.name | Step id | String | +| cloud.project.id | Jenkins plugin for that particular step | String | +| cloud.machine.type | Jenkins plugin version | String | +| cloud.region | The geographical region the resource is running | String | +| cloud.availability_zone | The zone where the resource is running | String | +| cloud.runAsUser | The user | String | +| cloud.platform | The cloud platform in use. | String | + + ##### Spans | Attribute | Description | Type | @@ -125,6 +142,16 @@ In addition, if the backends were configured then there will be an environment v | jenkins.url | Jenkins URL | String | | jenkins.computer.name | Name of the agent | String | +Containers/Kubernetes specific: + +| Attribute | Description | Type | +|----------------------------------|--------------|------| +| k8s.pod.name | The name of the Pod | String | +| container.image.name | The name of the image the container was built on | String | +| container.image.tag | Container image tag | String | +| container.name | The Container name | String | + + ### Metrics on Jenkins health indicators | Metrics | Unit | Label key | Label Value | Description | diff --git a/pom.xml b/pom.xml index 8f335d04b..44042e184 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 0.17 -SNAPSHOT - 2.235.5 + 2.249.3 8 jenkinsci/${project.artifactId}-plugin 1.2.0 @@ -61,6 +61,23 @@ pom import + + + commons-io + commons-io + 2.8.0 + + + + com.squareup.okhttp3 + okhttp + 3.14.9 + + + com.squareup.okhttp3 + logging-interceptor + 3.14.9 + @@ -216,6 +233,18 @@ git test + + org.jenkins-ci.plugins + google-compute-engine + 4.3.8 + true + + + org.csanchez.jenkins.plugins + kubernetes + 1.27.7 + true + org.jenkins-ci.plugins github-branch-source diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java index ee14fcc9f..7f0ae3584 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java @@ -15,6 +15,7 @@ import io.jenkins.plugins.opentelemetry.authentication.NoAuthentication; import io.jenkins.plugins.opentelemetry.authentication.OtlpAuthentication; import io.jenkins.plugins.opentelemetry.backend.ObservabilityBackend; +import io.jenkins.plugins.opentelemetry.computer.CloudSpanNamingStrategy; import io.jenkins.plugins.opentelemetry.job.SpanNamingStrategy; import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; import jenkins.model.GlobalConfiguration; @@ -73,6 +74,8 @@ public class JenkinsOpenTelemetryPluginConfiguration extends GlobalConfiguration private transient SpanNamingStrategy spanNamingStrategy; + private transient CloudSpanNamingStrategy cloudSpanNamingStrategy; + private transient ConcurrentMap loadedStepsPlugins = new ConcurrentHashMap<>(); private String serviceName; @@ -226,6 +229,15 @@ public SpanNamingStrategy getSpanNamingStrategy() { return spanNamingStrategy; } + @Inject + public void setCloudSpanNamingStrategy(CloudSpanNamingStrategy cloudSpanNamingStrategy) { + this.cloudSpanNamingStrategy = cloudSpanNamingStrategy; + } + + public CloudSpanNamingStrategy getCloudSpanNamingStrategy() { + return cloudSpanNamingStrategy; + } + @Nonnull public ConcurrentMap getLoadedStepsPlugins() { return loadedStepsPlugins; diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/CloudHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/CloudHandler.java new file mode 100644 index 000000000..d48ac3d8b --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/CloudHandler.java @@ -0,0 +1,22 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import hudson.model.Node; +import hudson.slaves.Cloud; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; + +import javax.annotation.Nonnull; + +public interface CloudHandler { + + boolean canAddAttributes(@Nonnull Cloud cloud); + boolean canAddAttributes(@Nonnull Node node); + void addCloudSpanAttributes(@Nonnull Node node, @Nonnull Span rootSpanBuilder) throws Exception; + void addCloudAttributes(@Nonnull Cloud cloud, @Nonnull SpanBuilder rootSpanBuilder) throws Exception; + String getCloudName(); +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/CloudSpanNamingStrategy.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/CloudSpanNamingStrategy.java new file mode 100644 index 000000000..a2b861776 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/CloudSpanNamingStrategy.java @@ -0,0 +1,44 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import com.google.common.annotations.VisibleForTesting; +import hudson.Extension; +import hudson.slaves.NodeProvisioner; +import jenkins.YesNoMaybe; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; + +/** + * Use same root span name for all pull cloud labels + */ +@Extension(dynamicLoadable = YesNoMaybe.YES) +@Symbol("cloudSpanNamingStrategy") +public class CloudSpanNamingStrategy { + + @DataBoundConstructor + public CloudSpanNamingStrategy() { + } + + @Nonnull + public String getRootSpanName(@Nonnull NodeProvisioner.PlannedNode plannedNode) { + return getNodeRootSpanName(plannedNode.displayName); + } + + @VisibleForTesting + @Nonnull + protected String getNodeRootSpanName(@Nonnull String displayName) { + // format: - + // e.g. "obs11-ubuntu-18-linux-beyyg2" + // remove last -<.*> + if (displayName.contains("-")) { + return displayName.substring(0, displayName.lastIndexOf("-")) + "-{id}"; + } + return displayName; + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/GoogleCloudHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/GoogleCloudHandler.java new file mode 100644 index 000000000..925dd9779 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/GoogleCloudHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import com.google.jenkins.plugins.computeengine.ComputeEngineCloud; +import com.google.jenkins.plugins.computeengine.ComputeEngineInstance; +import com.google.jenkins.plugins.computeengine.InstanceConfiguration; +import hudson.Extension; +import hudson.model.Node; +import hudson.slaves.Cloud; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import jenkins.YesNoMaybe; + +import javax.annotation.Nonnull; +import java.util.logging.Logger; + +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes.GOOGLE_CLOUD_PROVIDER; + +/** + * Customization of spans for google cloud attributes. + */ +@Extension(optional = true, dynamicLoadable = YesNoMaybe.YES) +public class GoogleCloudHandler implements CloudHandler { + private final static Logger LOGGER = Logger.getLogger(GoogleCloudHandler.class.getName()); + + @Override + public boolean canAddAttributes(@Nonnull Cloud cloud) { + return cloud.getDescriptor() instanceof ComputeEngineCloud.GoogleCloudDescriptor; + } + + @Override + public boolean canAddAttributes(@Nonnull Node node) { + return node instanceof ComputeEngineInstance; + } + + @Override + public void addCloudAttributes(@Nonnull Cloud cloud, @Nonnull SpanBuilder rootSpanBuilder) throws Exception { + ComputeEngineCloud ceCloud = (ComputeEngineCloud) cloud; + rootSpanBuilder + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_NAME, ceCloud.getCloudName()) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_PLATFORM, JenkinsOtelSemanticAttributes.GOOGLE_CLOUD_COMPUTE_ENGINE_PLATFORM) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_PROJECT_ID, ceCloud.getProjectId()) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_PROVIDER, GOOGLE_CLOUD_PROVIDER); + } + + @Override + public void addCloudSpanAttributes(@Nonnull Node node, @Nonnull Span span) throws Exception { + ComputeEngineInstance instance = (ComputeEngineInstance) node; + InstanceConfiguration configuration = instance.getCloud().getInstanceConfigurationByDescription(instance.getNodeDescription()); + if (configuration != null) { + span + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_ACCOUNT_ID, configuration.getServiceAccountEmail()) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_MACHINE_TYPE, transformMachineType(configuration.getMachineType())) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_REGION, transformRegion(configuration.getRegion())) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_RUN_AS_USER, configuration.getRunAsUser()) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_ZONE, transformZone(configuration.getZone())); + } + } + + @Override + public String getCloudName() { + return GOOGLE_CLOUD_PROVIDER; + } + + protected String transformRegion(String region) { + // f.e: "https://www.googleapis.com/compute/v1/projects/project-name/zones/us-central1-a" + return transform(region); + } + + protected String transformMachineType(String machineType) { + // f.e: "https://www.googleapis.com/compute/v1/projects/project-name/zones/us-central1-a/machineTypes/n2-standard-2" + return transform(machineType); + } + + protected String transformZone(String zone) { + // f.e: "https://www.googleapis.com/compute/v1/projects/project-name/zones/us-central1-a" + return transform(zone); + } + + private String transform(String value) { + if (value.contains("/")) { + return value.substring(value.lastIndexOf("/") + 1, value.length()); + } + return value; + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/KubernetesCloudHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/KubernetesCloudHandler.java new file mode 100644 index 000000000..2c14145a4 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/KubernetesCloudHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import hudson.Extension; +import hudson.model.Node; +import hudson.slaves.Cloud; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import jenkins.YesNoMaybe; +import org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud; +import org.csanchez.jenkins.plugins.kubernetes.KubernetesSlave; +import org.csanchez.jenkins.plugins.kubernetes.PodTemplate; + +import javax.annotation.Nonnull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes.K8S_CLOUD_PROVIDER; + +/** + * Customization of spans for kubernetes cloud attributes. + */ +@Extension(optional = true, dynamicLoadable = YesNoMaybe.YES) +public class KubernetesCloudHandler implements CloudHandler { + private final static Logger LOGGER = Logger.getLogger(KubernetesCloudHandler.class.getName()); + + @Nonnull + @Override + public boolean canAddAttributes(@Nonnull Cloud cloud) { + return cloud.getDescriptor() instanceof KubernetesCloud.DescriptorImpl; + } + + @Override + public boolean canAddAttributes(@Nonnull Node node) { + return node instanceof KubernetesSlave; + } + + @Override + public void addCloudAttributes(@Nonnull Cloud cloud, @Nonnull SpanBuilder rootSpanBuilder) throws Exception { + KubernetesCloud k8sCloud = (KubernetesCloud) cloud; + rootSpanBuilder + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_NAME, k8sCloud.getDisplayName()) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_PROJECT_ID, k8sCloud.getDisplayName()) + .setAttribute(JenkinsOtelSemanticAttributes.CLOUD_PROVIDER, K8S_CLOUD_PROVIDER) + .setAttribute(JenkinsOtelSemanticAttributes.K8S_NAMESPACE_NAME, k8sCloud.getNamespace()); + } + + @Override + public void addCloudSpanAttributes(@Nonnull Node node, @Nonnull Span span) throws Exception { + KubernetesSlave instance = (KubernetesSlave) node; + span + .setAttribute(JenkinsOtelSemanticAttributes.K8S_POD_NAME, instance.getPodName()); + PodTemplate podTemplate = instance.getTemplateOrNull(); + if (podTemplate != null) { + // TODO: add resourceLimit attributes to detect misbehaviours? + span + .setAttribute(JenkinsOtelSemanticAttributes.CONTAINER_IMAGE_NAME, getImageName(podTemplate.getImage())) + .setAttribute(JenkinsOtelSemanticAttributes.CONTAINER_IMAGE_TAG, getImageTag(podTemplate.getImage())) + .setAttribute(JenkinsOtelSemanticAttributes.CONTAINER_NAME, podTemplate.getName()); + } else { + LOGGER.log(Level.FINE, () -> "There is no podTemplate for the existing node."); + } + } + + @Override + public String getCloudName() { + return K8S_CLOUD_PROVIDER; + } + + protected String getImageName(String image) { + if (image.contains(":")) { + return image.substring(0, image.lastIndexOf(":")); + } + return image; + } + + protected String getImageTag(String image) { + if (image.contains(":")) { + return image.substring(image.lastIndexOf(":") + 1); + } + return "latest"; + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java index ea876bb08..13905b3f5 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java @@ -5,67 +5,196 @@ package io.jenkins.plugins.opentelemetry.computer; +import com.google.errorprone.annotations.MustBeClosed; +import com.google.inject.Inject; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.Label; import hudson.model.Node; -import hudson.slaves.CloudProvisioningListener; +import hudson.slaves.Cloud; import hudson.slaves.NodeProvisioner; -import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider; +import io.jenkins.plugins.opentelemetry.JenkinsOpenTelemetryPluginConfiguration; +import io.jenkins.plugins.opentelemetry.OtelUtils; +import io.jenkins.plugins.opentelemetry.computer.opentelemetry.OtelContextAwareAbstractCloudProvisioningListener; +import io.jenkins.plugins.opentelemetry.computer.opentelemetry.context.PlannedNodeContextKey; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; import io.jenkins.plugins.opentelemetry.semconv.JenkinsSemanticMetrics; import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; -import javax.inject.Inject; +import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; +import static com.google.common.base.Verify.verifyNotNull; + @Extension -public class MonitoringCloudListener extends CloudProvisioningListener { +public class MonitoringCloudListener extends OtelContextAwareAbstractCloudProvisioningListener { private final static Logger LOGGER = Logger.getLogger(MonitoringCloudListener.class.getName()); - protected Meter meter; - private LongCounter failureCloudCounter; private LongCounter totalCloudCount; + private CloudSpanNamingStrategy cloudSpanNamingStrategy; + @PostConstruct public void postConstruct() { - failureCloudCounter = meter.longCounterBuilder(JenkinsSemanticMetrics.JENKINS_CLOUD_AGENTS_FAILURE) + failureCloudCounter = getMeter().longCounterBuilder(JenkinsSemanticMetrics.JENKINS_CLOUD_AGENTS_FAILURE) .setDescription("Number of failed cloud agents when provisioning") .setUnit("1") .build(); - totalCloudCount = meter.longCounterBuilder(JenkinsSemanticMetrics.JENKINS_CLOUD_AGENTS_COMPLETED) + totalCloudCount = getMeter().longCounterBuilder(JenkinsSemanticMetrics.JENKINS_CLOUD_AGENTS_COMPLETED) .setDescription("Number of provisioned cloud agents") .setUnit("1") .build(); } @Override - public void onFailure(NodeProvisioner.PlannedNode plannedNode, Throwable t) { + public void _onStarted(Cloud cloud, Label label, Collection plannedNodes) { + LOGGER.log(Level.FINE, () -> "_onStarted(" + label + ")"); + for (NodeProvisioner.PlannedNode plannedNode : plannedNodes) { + _onStarted(cloud, label, plannedNode); + } + } + + public void _onStarted(Cloud cloud, Label label, NodeProvisioner.PlannedNode plannedNode) { + LOGGER.log(Level.FINE, () -> "_onStarted(label: " + label + ", label.nodes: " + label.getNodes().toString() + ", plannedNode: " + plannedNode + ")"); + + // Span name format: "(-)?-{id}" + // cloud is optional + // template-name is defined in the cloud configuration + // {id} is the low cardinality + String rootSpanName = getCloudNamePrefix(cloud) + this.cloudSpanNamingStrategy.getRootSpanName(plannedNode); + JenkinsOpenTelemetryPluginConfiguration.StepPlugin stepPlugin = JenkinsOpenTelemetryPluginConfiguration.get().findStepPluginOrDefault("cloud", cloud.getDescriptor()); + SpanBuilder rootSpanBuilder = getTracer().spanBuilder(rootSpanName).setSpanKind(SpanKind.SERVER); + + // TODO move this to a pluggable span enrichment API with implementations for different observability backends + // Regarding the value `unknown`, see https://github.com/jenkinsci/opentelemetry-plugin/issues/51 + rootSpanBuilder + .setAttribute(JenkinsOtelSemanticAttributes.ELASTIC_TRANSACTION_TYPE, "unknown") + .setAttribute(JenkinsOtelSemanticAttributes.CI_CLOUD_NAME, plannedNode.displayName) + .setAttribute(JenkinsOtelSemanticAttributes.CI_CLOUD_LABEL, label.getExpression()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_NAME, stepPlugin.getName()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_VERSION, stepPlugin.getVersion()); + + // ENRICH attributes with every Cloud specifics + for (CloudHandler cloudHandler : ExtensionList.lookup(CloudHandler.class)) { + if (cloudHandler.canAddAttributes(cloud)) { + try { + cloudHandler.addCloudAttributes(cloud, rootSpanBuilder); + } catch (Exception e) { + LOGGER.log(Level.WARNING, cloud.name + " failure to handle cloud provider with handler " + cloudHandler, e); + } + break; + } + } + + // START ROOT SPAN + Span rootSpan = rootSpanBuilder.startSpan(); + this.getTraceService().putSpan(plannedNode, rootSpan); + rootSpan.makeCurrent(); + LOGGER.log(Level.FINE, () -> plannedNode.displayName + " - begin root " + OtelUtils.toDebugString(rootSpan)); + } + + @Override + public void _onCommit(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node) { + LOGGER.log(Level.FINE, () -> "_onCommit(plannedNode: " + plannedNode + "node: " + node + ")"); + try (Scope parentScope = endCloudPhaseSpan(plannedNode)) { + Span span = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.CLOUD_SPAN_PHASE_COMMIT_NAME).setParent(Context.current()).startSpan(); + addCloudSpanAttributes(node, span); + span.setStatus(StatusCode.OK); + span.end(); + LOGGER.log(Level.FINE, () -> plannedNode.displayName + " - end _onCommit " + OtelUtils.toDebugString(span)); + } + } + + @Override + public void _onFailure(NodeProvisioner.PlannedNode plannedNode, Throwable t) { + LOGGER.log(Level.FINE, () -> "_onFailure(plannedNode: " + plannedNode + ")"); failureCloudCounter.add(1); - LOGGER.log(Level.FINE, () -> "onFailure(" + plannedNode + ")"); + try (Scope parentScope = endCloudPhaseSpan(plannedNode)) { + Span span = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.CLOUD_SPAN_PHASE_FAILURE_NAME).setParent(Context.current()).startSpan(); + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getMessage()); + span.end(); + LOGGER.log(Level.FINE, () -> plannedNode.displayName + " - end _onFailure " + OtelUtils.toDebugString(span)); + } } @Override - public void onRollback(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node, - @NonNull Throwable t) { + public void _onRollback(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node, + @NonNull Throwable t){ + LOGGER.log(Level.FINE, () -> "_onRollback(plannedNode" + plannedNode + ", node: " + node + ")"); failureCloudCounter.add(1); - LOGGER.log(Level.FINE, () -> "onRollback(" + plannedNode + ")"); + try (Scope parentScope = endCloudPhaseSpan(plannedNode)) { + Span span = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.CLOUD_SPAN_PHASE_FAILURE_NAME).setParent(Context.current()).startSpan(); + addCloudSpanAttributes(node, span); + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getMessage()); + span.end(); + LOGGER.log(Level.FINE, () -> plannedNode.displayName + " - end _onRollback " + OtelUtils.toDebugString(span)); + } } @Override - public void onComplete(NodeProvisioner.PlannedNode plannedNode, Node node) { + public void _onComplete(NodeProvisioner.PlannedNode plannedNode, Node node) { + LOGGER.log(Level.FINE, () -> "_onComplete(plannedNode: " + plannedNode + ", node: " + node + ")"); totalCloudCount.add(1); - LOGGER.log(Level.FINE, () -> "onComplete(" + plannedNode + ")"); + try (Scope parentScope = endCloudPhaseSpan(plannedNode)) { + Span span = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.CLOUD_SPAN_PHASE_COMPLETE_NAME).setParent(Context.current()).startSpan(); + addCloudSpanAttributes(node, span); + span.setStatus(StatusCode.OK); + span.end(); + LOGGER.log(Level.FINE, () -> plannedNode.displayName + " - end _onComplete " + OtelUtils.toDebugString(span)); + } + } + + @MustBeClosed + @Nonnull + protected Scope endCloudPhaseSpan(@NonNull NodeProvisioner.PlannedNode plannedNode) { + Span cloudPhaseSpan = verifyNotNull(Span.current(), "No cloudPhaseSpan found in context"); + cloudPhaseSpan.end(); + LOGGER.log(Level.FINE, () -> plannedNode.displayName + " - end " + OtelUtils.toDebugString(cloudPhaseSpan)); + + Span newCurrentSpan = this.getTraceService().getSpan(plannedNode); + Scope newScope = newCurrentSpan.makeCurrent(); + Context.current().with(PlannedNodeContextKey.KEY, plannedNode); + return newScope; + } + + private void addCloudSpanAttributes(@NonNull Node node, @NonNull Span span) { + // ENRICH attributes with every Cloud Node specifics + for (CloudHandler cloudHandler : ExtensionList.lookup(CloudHandler.class)) { + if (cloudHandler.canAddAttributes(node)) { + try { + cloudHandler.addCloudSpanAttributes(node, span); + } catch (Exception e) { + LOGGER.log(Level.WARNING, node.getNodeName() + " failure to handle node provider with handler " + cloudHandler, e); + } + break; + } + } + } + + private String getCloudNamePrefix(@NonNull Cloud cloud) { + for (CloudHandler cloudHandler : ExtensionList.lookup(CloudHandler.class)) { + if (cloudHandler.canAddAttributes(cloud)) { + return cloudHandler.getCloudName() + "-"; + } + } + return ""; } - /** - * Jenkins doesn't support {@link com.google.inject.Provides} so we manually wire dependencies :-( - */ @Inject - public void setMeter(@Nonnull OpenTelemetrySdkProvider openTelemetrySdkProvider) { - this.meter = openTelemetrySdkProvider.getMeter(); + public void setCloudSpanNamingStrategy(CloudSpanNamingStrategy cloudSpanNamingStrategy) { + this.cloudSpanNamingStrategy = cloudSpanNamingStrategy; } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/OtelTraceService.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/OtelTraceService.java new file mode 100644 index 000000000..86881e3aa --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/OtelTraceService.java @@ -0,0 +1,269 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.errorprone.annotations.MustBeClosed; +import hudson.Extension; +import hudson.slaves.NodeProvisioner; +import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider; +import io.jenkins.plugins.opentelemetry.computer.opentelemetry.context.PlannedNodeContextKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.google.common.base.Verify.verify; + +@Extension +public class OtelTraceService { + + private static Logger LOGGER = Logger.getLogger(OtelTraceService.class.getName()); + + private transient ConcurrentMap spansByCloud; + + private Tracer tracer; + + private Tracer noOpTracer; + + public OtelTraceService() { + initialize(); + } + + protected Object readResolve() { + initialize(); + return this; + } + + private void initialize() { + spansByCloud = new ConcurrentHashMap(); + } + + @Nonnull + public Span getSpan(@Nonnull NodeProvisioner.PlannedNode plannedNode) { + return getSpan(plannedNode, true); + } + + @Nonnull + public Span getSpan(@Nonnull NodeProvisioner.PlannedNode plannedNode, boolean verifyIfRemainingSteps) { + PlannedNodeIdentifier plannedNodeIdentifier = PlannedNodeIdentifier.fromRun(plannedNode); + CloudSpans cloudSpans = spansByCloud.computeIfAbsent(plannedNodeIdentifier, plannedNodeIdentifier1 -> new CloudSpans()); // absent when Jenkins restarts during build + + if (verifyIfRemainingSteps) { + verify(cloudSpans.cloudSpansByFlowNodeId.isEmpty(), plannedNode.displayName + " - Can't access run phase span while there are remaining cloud step spans: " + cloudSpans); + } + LOGGER.log(Level.FINEST, () -> "getSpan(" + plannedNode.displayName + ") - " + cloudSpans); + final Span span = Iterables.getLast(cloudSpans.runPhasesSpans, null); + if (span == null) { + LOGGER.log(Level.FINE, () -> "No span found for run " + plannedNode.displayName+ ", Jenkins server may have restarted"); + return noOpTracer.spanBuilder("noop-recovery-planned-node-span-for-" + plannedNode.displayName).startSpan(); + } + return span; + } + + @Nonnull + public Span getSpan(@Nonnull NodeProvisioner.PlannedNode plannedNode, hudson.model.Node flowNode) { + PlannedNodeIdentifier plannedNodeIdentifier = PlannedNodeIdentifier.fromRun(plannedNode); + CloudSpans cloudSpans = spansByCloud.computeIfAbsent(plannedNodeIdentifier, plannedNodeIdentifier1 -> new CloudSpans()); // absent when Jenkins restarts during build + LOGGER.log(Level.FINEST, () -> "getSpan(" + plannedNode.displayName + ", Node[displayName" + flowNode.getDisplayName() + ", nodeName:" + flowNode.getNodeName() + ", label=" + flowNode.getLabelString() + "]) - " + cloudSpans); + + final Span span = Iterables.getLast(cloudSpans.runPhasesSpans, null); + if (span == null) { + LOGGER.log(Level.FINE, () -> "No span found for run " + plannedNode.displayName + ", Jenkins server may have restarted"); + return noOpTracer.spanBuilder("noop-recovery-planned-node-span-for-" + plannedNode.displayName).startSpan(); + } + LOGGER.log(Level.FINEST, () -> "span: " + span.getSpanContext().getSpanId()); + return span; + } + + public void putSpan(@Nonnull NodeProvisioner.PlannedNode plannedNode, @Nonnull Span span) { + PlannedNodeIdentifier plannedNodeIdentifier = PlannedNodeIdentifier.fromRun(plannedNode); + CloudSpans cloudSpans = spansByCloud.computeIfAbsent(plannedNodeIdentifier, plannedNodeIdentifier1 -> new CloudSpans()); + cloudSpans.runPhasesSpans.add(span); + + LOGGER.log(Level.FINEST, () -> "putSpan(" + plannedNode.displayName + "," + span + ") - new stack: " + cloudSpans); + } + + public void putSpan(@Nonnull NodeProvisioner.PlannedNode plannedNode, @Nonnull Span span, @Nonnull hudson.model.Node flowNode) { + PlannedNodeIdentifier plannedNodeIdentifier = PlannedNodeIdentifier.fromRun(plannedNode); + CloudSpans cloudSpans = spansByCloud.computeIfAbsent(plannedNodeIdentifier, plannedNodeIdentifier1 -> new CloudSpans()); + cloudSpans.cloudSpansByFlowNodeId.put(flowNode.getDisplayName(), new CloudSpanContext(span, flowNode)); + + LOGGER.log(Level.FINEST, () -> "putSpan(" + plannedNode.displayName + ", Node[displayName" + flowNode.getDisplayName() + ", nodeName:" + flowNode.getNodeName() + ", label=" + flowNode.getLabelString() + "], Span[id: " + span.getSpanContext().getSpanId() + "]" + ") - " + cloudSpans); + } + + @Inject + public void setJenkinsOtelPlugin(@Nonnull OpenTelemetrySdkProvider openTelemetrySdkProvider) { + this.tracer = openTelemetrySdkProvider.getTracer(); + this.noOpTracer = TracerProvider.noop().get("jenkins"); + } + + /** + * @param plannedNodes the planned nodes + * @return If no span has been found (ie Jenkins restart), then the scope of a NoOp span is returned + */ + @Nonnull + @MustBeClosed + public Scope setupContext(@Nonnull Collection plannedNodes) { + if (plannedNodes.size() == 1) { + return setupContext(plannedNodes.iterator().next()); + } + return noOpTracer.spanBuilder("noop-multiple-planned-nodes-span").startSpan().makeCurrent(); + } + + /** + * @param plannedNode the planned node + * @return If no span has been found (ie Jenkins restart), then the scope of a NoOp span is returned + */ + @Nonnull + @MustBeClosed + public Scope setupContext(@Nullable NodeProvisioner.PlannedNode plannedNode) { + if (plannedNode != null) { + Span span = getSpan(plannedNode); + Scope scope = span.makeCurrent(); + Context.current().with(PlannedNodeContextKey.KEY, plannedNode); + return scope; + } + return noOpTracer.spanBuilder("noop-existing-planned-nodes-span").startSpan().makeCurrent(); + } + + public Tracer getTracer() { + return tracer; + } + + + @Immutable + public static class CloudSpans { + final Multimap cloudSpansByFlowNodeId = ArrayListMultimap.create(); + final List runPhasesSpans = new ArrayList<>(); + + @Override + public String toString() { + // clone the Multimap to prevent a ConcurrentModificationException + // see https://github.com/jenkinsci/opentelemetry-plugin/issues/129 + return "CloudSpans{" + + "runPhasesSpans=" + Collections.unmodifiableList(runPhasesSpans) + + ", pipelineStepSpansByFlowNodeId=" + ArrayListMultimap.create(cloudSpansByFlowNodeId) + + '}'; + } + } + + public static class CloudSpanContext { + final transient Span span; + final String flowNodeId; + final List parentFlowNodeIds; + + public CloudSpanContext(@Nonnull Span span, @Nonnull hudson.model.Node flowNode) { + this.span = span; + this.flowNodeId = flowNode.getNodeName(); + this.parentFlowNodeIds = new ArrayList<>( 1); + this.parentFlowNodeIds.add(flowNode.getNodeName()); + } + + /** + * Return the stack of the parent {@link FlowNode} of this {@link Span}. + * The first id of the list is the {@link FlowNode} on which the {@link Span} has been created, the last item of the list if the oldest parent. + * + * @see FlowNode#getParents() + */ + @Nonnull + public List getParentFlowNodeIds() { + return parentFlowNodeIds; + } + + /** + * FIXME handle cases where the data structure has been deserialized and {@link Span} is null. + */ + @Nonnull + public Span getSpan() { + return span; + } + + @Override + public String toString() { + return "CloudSpanContext{" + + "span=" + span + + "flowNodeId=" + flowNodeId + + ", parentIds=" + parentFlowNodeIds + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CloudSpanContext that = (CloudSpanContext) o; + return Objects.equals(this.span.getSpanContext().getSpanId(), that.span.getSpanContext().getSpanId()) && flowNodeId.equals(that.flowNodeId); + } + + @Override + public int hashCode() { + return Objects.hash(span.getSpanContext().getSpanId(), flowNodeId); + } + } + + @Immutable + public static class PlannedNodeIdentifier implements Comparable { + final String nodeName; + + static PlannedNodeIdentifier fromRun(@Nonnull NodeProvisioner.PlannedNode run) { + return new PlannedNodeIdentifier(run.displayName); + } + + public PlannedNodeIdentifier(@Nonnull String nodeName) { + this.nodeName = nodeName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlannedNodeIdentifier that = (PlannedNodeIdentifier) o; + return nodeName.equals(that.nodeName); + } + + @Override + public int hashCode() { + return Objects.hash(nodeName); + } + + @Override + public String toString() { + return "PlannedNodeIdentifier{" + + "nodeName='" + nodeName + '\'' + + '}'; + } + + public String getNodeName() { + return nodeName; + } + + + @Override + public int compareTo(PlannedNodeIdentifier o) { + return ComparisonChain.start().compare(this.nodeName, o.nodeName).result(); + } + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/OtelContextAwareAbstractCloudProvisioningListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/OtelContextAwareAbstractCloudProvisioningListener.java new file mode 100644 index 000000000..8bb16e33b --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/OtelContextAwareAbstractCloudProvisioningListener.java @@ -0,0 +1,113 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer.opentelemetry; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Label; +import hudson.model.Node; +import hudson.slaves.Cloud; +import hudson.slaves.CloudProvisioningListener; +import hudson.slaves.NodeProvisioner; +import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider; +import io.jenkins.plugins.opentelemetry.computer.OtelTraceService; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link CloudProvisioningListener} that setups the OpenTelemetry {@link io.opentelemetry.context.Context} + * with the current {@link Span}. + */ +public abstract class OtelContextAwareAbstractCloudProvisioningListener extends CloudProvisioningListener { + + private final static Logger LOGGER = Logger.getLogger(OtelContextAwareAbstractCloudProvisioningListener.class.getName()); + + private OtelTraceService otelTraceService; + private Tracer tracer; + private Meter meter; + + @Inject + public final void setOpenTelemetryTracerService(@Nonnull OtelTraceService otelTraceService, @Nonnull OpenTelemetrySdkProvider openTelemetrySdkProvider) { + LOGGER.log(Level.FINE, () -> "setOpenTelemetryTracerService()"); + this.otelTraceService = otelTraceService; + this.tracer = this.otelTraceService.getTracer(); + this.meter = openTelemetrySdkProvider.getMeter(); + } + + @Override + public void onStarted(Cloud cloud, Label label, Collection plannedNodes) { + LOGGER.log(Level.FINE, () -> "onStarted(" + label.getExpression() + ")"); + this._onStarted(cloud, label, plannedNodes); + } + + public void _onStarted(Cloud cloud, Label label, Collection plannedNodes) { + } + + @Override + public void onComplete(NodeProvisioner.PlannedNode plannedNode, Node node) { + LOGGER.log(Level.FINE, () -> "onComplete(" + node + ")"); + try (Scope scope = getTraceService().setupContext(plannedNode)) { + this._onComplete(plannedNode, node); + } + } + + public void _onComplete(NodeProvisioner.PlannedNode plannedNode, Node node) { + } + + @Override + public void onCommit(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node) { + LOGGER.log(Level.FINE, () -> "onCommit(" + node + ")"); + try (Scope scope = getTraceService().setupContext(plannedNode)) { + this._onCommit(plannedNode, node); + } + } + + public void _onCommit(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node) { + } + + @Override + public void onFailure(NodeProvisioner.PlannedNode plannedNode, Throwable t) { + LOGGER.log(Level.FINE, () -> "onFailure(" + plannedNode + ")"); + try (Scope scope = getTraceService().setupContext(plannedNode)) { + this._onFailure(plannedNode, t); + } + } + + public void _onFailure(NodeProvisioner.PlannedNode plannedNode, Throwable t) { + } + + @Override + public void onRollback(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node, + @NonNull Throwable t) { + LOGGER.log(Level.FINE, () -> "onRollback(" + node + ")"); + try (Scope scope = getTraceService().setupContext(plannedNode)) { + this._onRollback(plannedNode, node, t); + } + } + + public void _onRollback(@NonNull NodeProvisioner.PlannedNode plannedNode, @NonNull Node node, + @NonNull Throwable t) { + } + + public OtelTraceService getTraceService() { + return otelTraceService; + } + + public Tracer getTracer() { + return tracer; + } + + public Meter getMeter() { + return meter; + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/context/FlowNodeContextKey.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/context/FlowNodeContextKey.java new file mode 100644 index 000000000..657015cf7 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/context/FlowNodeContextKey.java @@ -0,0 +1,21 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer.opentelemetry.context; + +import io.opentelemetry.context.ContextKey; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +import javax.annotation.concurrent.Immutable; + +/** + * See {@code io.opentelemetry.api.trace.SpanContextKey} + */ +@Immutable +public final class FlowNodeContextKey { + public static final ContextKey KEY = ContextKey.named(FlowNodeContextKey.class.getName()); + + private FlowNodeContextKey(){} +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/context/PlannedNodeContextKey.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/context/PlannedNodeContextKey.java new file mode 100644 index 000000000..e3aa703be --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/opentelemetry/context/PlannedNodeContextKey.java @@ -0,0 +1,21 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer.opentelemetry.context; + +import hudson.slaves.NodeProvisioner; +import io.opentelemetry.context.ContextKey; + +import javax.annotation.concurrent.Immutable; + +/** + * See {@code io.opentelemetry.api.trace.SpanContextKey} + */ +@Immutable +public final class PlannedNodeContextKey { + public static final ContextKey KEY = ContextKey.named(NodeProvisioner.PlannedNode.class.getName()); + + private PlannedNodeContextKey(){} +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java index fcadb9f8c..c3468421e 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java @@ -114,4 +114,40 @@ public final class JenkinsOtelSemanticAttributes { public static final AttributeKey ELASTIC_TRANSACTION_TYPE = AttributeKey.stringKey("type"); + public static final AttributeKey CI_CLOUD_LABEL = AttributeKey.stringKey("ci.cloud.label"); + public static final AttributeKey CI_CLOUD_NAME = AttributeKey.stringKey("ci.cloud.name"); + /** + * @see cloud semantic conventions + */ + public static final AttributeKey CLOUD_ACCOUNT_ID = AttributeKey.stringKey("cloud.account.id"); + public static final AttributeKey CLOUD_PROVIDER = AttributeKey.stringKey("cloud.provider"); + public static final AttributeKey CLOUD_NAME = AttributeKey.stringKey("cloud.name"); + public static final AttributeKey CLOUD_PROJECT_ID = AttributeKey.stringKey("cloud.project.id"); + public static final AttributeKey CLOUD_MACHINE_TYPE = AttributeKey.stringKey("cloud.machine.type"); + public static final AttributeKey CLOUD_REGION = AttributeKey.stringKey("cloud.region"); + public static final AttributeKey CLOUD_RUN_AS_USER = AttributeKey.stringKey("cloud.runAsUser"); + public static final AttributeKey CLOUD_ZONE = AttributeKey.stringKey("cloud.availability_zone"); + public static final AttributeKey CLOUD_PLATFORM = AttributeKey.stringKey("cloud.platform"); + public static final String CLOUD_SPAN_PHASE_STARTED_NAME = "Phase: Started"; + public static final String CLOUD_SPAN_PHASE_COMMIT_NAME = "Phase: Commit"; + public static final String CLOUD_SPAN_PHASE_FAILURE_NAME = "Phase: Failure"; + public static final String CLOUD_SPAN_PHASE_COMPLETE_NAME = "Phase: Complete"; + + public static final String GOOGLE_CLOUD_PROVIDER = "gcp"; + public static final String GOOGLE_CLOUD_COMPUTE_ENGINE_PLATFORM = "gcp_compute_engine"; + + /** + * @see kubernetes semantic conventions + */ + public static final AttributeKey K8S_NAMESPACE_NAME = AttributeKey.stringKey("k8s.namespace.name"); + public static final AttributeKey K8S_POD_NAME = AttributeKey.stringKey("k8s.pod.name"); + + /** + * @see container semantic conventions + */ + public static final AttributeKey CONTAINER_IMAGE_NAME = AttributeKey.stringKey("container.image.name"); + public static final AttributeKey CONTAINER_IMAGE_TAG = AttributeKey.stringKey("container.image.tag"); + public static final AttributeKey CONTAINER_NAME = AttributeKey.stringKey("container.name"); + + public static final String K8S_CLOUD_PROVIDER = "k8s"; } diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/computer/CloudSpanNamingStrategyTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/computer/CloudSpanNamingStrategyTest.java new file mode 100644 index 000000000..1960041fd --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/computer/CloudSpanNamingStrategyTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import org.junit.Assert; +import org.junit.Test; + +public class CloudSpanNamingStrategyTest { + + CloudSpanNamingStrategy spanNamingStrategy = new CloudSpanNamingStrategy(); + + /** + * Unique node + */ + @Test + public void test_default_name() { + verifyNodeRootSpanName("foo", "foo"); + } + + /** + * Dynamic node + */ + @Test + public void test_with_dynamic_node() { + verifyNodeRootSpanName("obs11-ubuntu-18-linux-beyyg2", "obs11-ubuntu-18-linux-{id}"); + } + + private void verifyNodeRootSpanName(String displayName, String expected) { + final String actual = spanNamingStrategy.getNodeRootSpanName(displayName); + Assert.assertEquals(expected, actual); + } +} \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/computer/GoogleCloudHandlerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/computer/GoogleCloudHandlerTest.java new file mode 100644 index 000000000..01e9906bd --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/computer/GoogleCloudHandlerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import org.junit.Assert; +import org.junit.Test; + +public class GoogleCloudHandlerTest { + + GoogleCloudHandler handler = new GoogleCloudHandler(); + + @Test + public void test_default_region() { + verifyRegion("foo", "foo"); + } + + @Test + public void test_region() { + verifyRegion("https://www.googleapis.com/compute/v1/projects/project-name/regions/us-central1", "us-central1"); + } + + @Test + public void test_default_zone() { + verifyZone("foo", "foo"); + } + + @Test + public void test_zone() { + verifyZone("https://www.googleapis.com/compute/v1/projects/elastic-observability/zones/us-central1-a", "us-central1-a"); + } + + @Test + public void test_default_machineType() { + verifyMachineType("foo", "foo"); + } + + @Test + public void test_machineType() { + verifyMachineType("https://www.googleapis.com/compute/v1/projects/project-name/zones/us-central1-a/machineTypes/n2-standard-2", "n2-standard-2"); + } + + private void verifyMachineType(String machineType, String expected) { + final String actual = handler.transformMachineType(machineType); + Assert.assertEquals(expected, actual); + } + + private void verifyRegion(String region, String expected) { + final String actual = handler.transformRegion(region); + Assert.assertEquals(expected, actual); + } + + private void verifyZone(String zone, String expected) { + final String actual = handler.transformZone(zone); + Assert.assertEquals(expected, actual); + } +} \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/computer/KubernetesCloudHandlerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/computer/KubernetesCloudHandlerTest.java new file mode 100644 index 000000000..4091b05aa --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/computer/KubernetesCloudHandlerTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.computer; + +import org.junit.Assert; +import org.junit.Test; + +public class KubernetesCloudHandlerTest { + + KubernetesCloudHandler handler = new KubernetesCloudHandler(); + + @Test + public void test_default_image() { + verifyImage("foo", "foo"); + } + + @Test + public void test_image_with_tag() { + verifyImage("org/foo:1.15.10", "org/foo"); + } + + @Test + public void test_default_image_tag() { + verifyImageTag("foo", "latest"); + } + + @Test + public void test_imagetag_with_tag() { + verifyImageTag("org/foo:1.15.10", "1.15.10"); + } + + private void verifyImage(String image, String expected) { + final String actual = handler.getImageName(image); + Assert.assertEquals(expected, actual); + } + + private void verifyImageTag(String image, String expected) { + final String actual = handler.getImageTag(image); + Assert.assertEquals(expected, actual); + } +} \ No newline at end of file