diff --git a/.github/workflows/ci-feature.yml b/.github/workflows/ci-feature.yml deleted file mode 100644 index a8f5984..0000000 --- a/.github/workflows/ci-feature.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Feature Branch Continuous Integration - -on: - push: - branches: - - "feature/**" - pull_request: - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - build-and-test: - name: "Build and test extension" - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies. - # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - - name: Setup Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 - with: - build-scan-publish: true - build-scan-terms-of-service-url: 'https://gradle.com/terms-of-service' - build-scan-terms-of-service-agree: 'yes' - - - name: Test - run: ./gradlew test - - - name: Build - run: ./gradlew extendedAgent - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: Package - path: inspectit-gepard-agent/build/libs - - dependency-submission: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies. - # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md - - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 diff --git a/.github/workflows/cr-master.yml b/.github/workflows/ci-test-and-push-docker.yml similarity index 77% rename from .github/workflows/cr-master.yml rename to .github/workflows/ci-test-and-push-docker.yml index 7cfa1e6..fb422bc 100644 --- a/.github/workflows/cr-master.yml +++ b/.github/workflows/ci-test-and-push-docker.yml @@ -1,11 +1,11 @@ -# This workflow is triggered on pushes to the master branch. -# It builds the agent and creates a multi-arch image for it. +# This workflow builds the agent and creates a multi-arch image for it. # The image is then pushed to Docker Hub. # -# The workflow consists of three jobs: -# 1. build-agent: Builds the agent and uploads the resulting JAR as an artifact. -# 2. build-image: Builds a Docker image for multiple platforms with the matrix strategy. -# 3. merge: Merges the images for the different platforms into a manifest list and pushes it to Docker Hub. +# The workflow consists of the following jobs: +# 1. build-agent-jar: Builds the agent and uploads the resulting JAR as an artifact. +# 2. dependency-submission: Generate a dependency graph. +# 2. build-docker-images: Builds a Docker image for multiple platforms with the matrix strategy. +# 3. merge-images: Merges the images for the different platforms into a manifest list and pushes it to Docker Hub. # To make the build faster, we use a matrix strategy to build the image for multiple platforms in parallel. # The build of the first job is copied over to the image build jobs, so that the application build is only done once. @@ -14,18 +14,17 @@ # For more information about how to build multi-arch images and advanced settings with Docker Buildx in GitHub actions, see: # https://docs.docker.com/build/ci/github-actions/multi-platform/ -name: Master Branch Continuous Release +name: Branch Continuous Integration on: push: - branches: - - master env: REGISTRY_IMAGE: inspectit/inspectit-gepard-agent + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: - build-agent: + build-agent-jar: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -56,9 +55,26 @@ jobs: name: agent-artifact path: inspectit-gepard-agent/build/libs/inspectit-gepard-agent.jar - build-image: + dependency-submission: runs-on: ubuntu-latest - needs: build-agent + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies. + # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + build-docker-images: + runs-on: ubuntu-latest + needs: build-agent-jar strategy: fail-fast: false matrix: @@ -124,10 +140,9 @@ jobs: if-no-files-found: error retention-days: 1 - merge: + merge-images: runs-on: ubuntu-latest - needs: - - build-image + needs: build-docker-images steps: - name: Download digests uses: actions/download-artifact@v4 diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java index 44f28a6..fb574e4 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java @@ -1,54 +1,52 @@ /* (C) 2024 */ package rocks.inspectit.gepard.agent.instrumentation.hook.action; -import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import rocks.inspectit.gepard.agent.instrumentation.hook.action.exception.CouldNotCloseSpanScopeException; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.util.SpanUtil; import rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor; /** This action contains the logic to start and end a {@link Span}. */ public class SpanAction { - - private static final String INSTRUMENTATION_SCOPE_NAME = "inspectit-gepard"; - - private final OpenTelemetry openTelemetry; - - public SpanAction() { - this.openTelemetry = OpenTelemetryAccessor.getOpenTelemetry(); - } + private static final Logger log = LoggerFactory.getLogger(SpanAction.class); /** * Starts a new {@link Span}. Should be called before {@link SpanAction#endSpan}. * * @param spanName the name of the span - * @return the scope of the started span + * @return the scope of the started span or null, if the current span has the same name */ public AutoCloseable startSpan(String spanName) { - Span.current().getSpanContext(); + // In the future, we still might want to set some attributes in the current span + if (SpanUtil.spanAlreadyExists(spanName)) { + log.debug("Span '{}' already exists at the moment. No new span will be started", spanName); + return null; + } - Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_SCOPE_NAME); + Tracer tracer = OpenTelemetryAccessor.getTracer(); Span span = tracer.spanBuilder(spanName).setParent(Context.current()).startSpan(); return span.makeCurrent(); } /** * Ends the current span and closes its scope. Should be called after {@link - * SpanAction#startSpan}. + * SpanAction#startSpan}. If the scope is null, we won't do anything. * * @param spanScope the scope of the span, which should be finished */ public void endSpan(AutoCloseable spanScope) { - Span current = Span.current(); + if (Objects.isNull(spanScope)) return; - if (Objects.nonNull(spanScope)) { - try { - spanScope.close(); - } catch (Exception e) { - throw new CouldNotCloseSpanScopeException(e); - } + Span current = Span.current(); + try { + spanScope.close(); + } catch (Exception e) { + throw new CouldNotCloseSpanScopeException(e); } current.end(); } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/util/SpanUtil.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/util/SpanUtil.java new file mode 100644 index 0000000..09945c7 --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/util/SpanUtil.java @@ -0,0 +1,27 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.action.util; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.trace.ReadableSpan; + +/** Util class to access span data, like the name or attributes. */ +public class SpanUtil { + + private SpanUtil() {} + + /** + * Checks, if the current span uses the provided span name. This check might be necessary to + * prevent span duplicates. For example, we would like to create a span for a method, which is + * already recorded by OpenTelemetry. In this case, we should not create a new span. + * + * @param spanName the name of the span with the format 'SimpleClassName.methodName', for instance + * 'SpanUtil.spanAlreadyExists' + * @return true, if the current span uses the provided span name + */ + public static boolean spanAlreadyExists(String spanName) { + Span span = Span.current(); + + if (span instanceof ReadableSpan readableSpan) return spanName.equals(readableSpan.getName()); + return false; + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java index f95865f..2da215a 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java @@ -3,25 +3,23 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; /** - * Singleton to access the {@link OpenTelemetry} instance. We use this accessor, because according - * to the documentation of {@link GlobalOpenTelemetry}, the get() method should only be called once - * during the application. + * Singleton to access the OpenTelemetry API. We use this accessor, because according to the + * documentation of {@link GlobalOpenTelemetry}, the get() method should only be called once during + * the application. */ public class OpenTelemetryAccessor { + /** The instrumentation scope name we use for our spans and metrics */ + public static final String INSTRUMENTATION_SCOPE_NAME = "inspectit-gepard"; + + /** Our global OpenTelemetry instance */ private static OpenTelemetry openTelemetry; private OpenTelemetryAccessor() {} - /** - * @return the global {@link OpenTelemetry} instance - */ - public static OpenTelemetry getOpenTelemetry() { - return openTelemetry; - } - /** * Sets the global {@link OpenTelemetry} instance for inspectIT. This will allow us to create * traces or metrics. Should only be called once. @@ -31,4 +29,11 @@ public static OpenTelemetry getOpenTelemetry() { public static void setOpenTelemetry(OpenTelemetry otel) { openTelemetry = otel; } + + /** + * @return the tracer to create spans + */ + public static Tracer getTracer() { + return openTelemetry.getTracer(INSTRUMENTATION_SCOPE_NAME); + } } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java index 823ab4b..7dc6e2a 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java @@ -46,7 +46,7 @@ public AgentBuilder modify(AgentBuilder agentBuilder) { */ private ElementMatcher.Junction typeMatcher() { return not( - nameStartsWith("rocks.inspectit") + nameStartsWith("rocks.inspectit.gepard.agent") .or(nameStartsWith("java.lang.invoke")) .or(nameStartsWith("net.bytebuddy"))); } diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/util/SpanUtilTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/util/SpanUtilTest.java new file mode 100644 index 0000000..5018219 --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/util/SpanUtilTest.java @@ -0,0 +1,61 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.action.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SpanUtilTest { + + private Tracer tracer; + + private final String spanName = "SpanUtilTest.method"; + + @BeforeEach + void beforeEach() { + GlobalOpenTelemetry.resetForTest(); + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().buildAndRegisterGlobal(); + tracer = openTelemetry.getTracer("inspectit-gepard"); + } + + @Test + void shouldReturnTrueWhenSpanExists() throws Exception { + Span span = tracer.spanBuilder(spanName).startSpan(); + Scope scope = span.makeCurrent(); + + boolean exists = SpanUtil.spanAlreadyExists(spanName); + + assertTrue(exists); + + scope.close(); + span.end(); + } + + @Test + void shouldReturnFalseWhenNoSpanExists() throws Exception { + boolean exists = SpanUtil.spanAlreadyExists(spanName); + + assertFalse(exists); + } + + @Test + void shouldReturnFalseWhenOtherSpanExists() throws Exception { + Span span = tracer.spanBuilder("dummy").startSpan(); + Scope scope = span.makeCurrent(); + + boolean exists = SpanUtil.spanAlreadyExists(spanName); + + assertFalse(exists); + + scope.close(); + span.end(); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java index 93d2d19..34096a9 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java @@ -1,15 +1,15 @@ /* (C) 2024 */ package rocks.inspectit.gepard.agent.integrationtest.spring.trace; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor.INSTRUMENTATION_SCOPE_NAME; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.ScopeSpans; import io.opentelemetry.proto.trace.v1.Span; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.function.Predicate; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import rocks.inspectit.gepard.agent.integrationtest.spring.SpringTestBase; @@ -21,12 +21,6 @@ class TracingTest extends SpringTestBase { private static final String childSpanName = "WebController.withSpan"; - @Override - protected Map getExtraEnv() { - // We need to disable the OTel annotations to prevent span duplicates - return Map.of("OTEL_INSTRUMENTATION_OPENTELEMETRY_EXTENSION_ANNOTATIONS_ENABLED", "false"); - } - @Test void shouldSendSpansToBackendWhenScopesAreActive() throws Exception { configurationServerMock.configServerSetup(configDir + "simple-config.json"); @@ -52,8 +46,8 @@ void shouldNotSendSpansToBackendWhenNoScopesAreActive() throws Exception { } /** - * This method asserts that spans with the given names exist and that the child's {@link - * Span#getParentSpanId()} equals the parent's {@link Span#getSpanId()}. + * This method asserts that spans with the given names exist, they appear in the same trace and + * that the child's {@link Span#getParentSpanId()} equals the parent's {@link Span#getSpanId()}. * * @param traces the collection of traces * @param parentSpanName the name of the parent span @@ -61,26 +55,26 @@ void shouldNotSendSpansToBackendWhenNoScopesAreActive() throws Exception { */ private void assertSpans( Collection traces, String parentSpanName, String childSpanName) { - Stream> spanLists = getSpanLists(traces); - - assertTrue( - spanLists.anyMatch( - spans -> { - Optional parentSpan = findSpan(spans, parentSpanName); - if (parentSpan.isEmpty()) return false; - - Optional childSpan = findSpan(spans, childSpanName); - if (childSpan.isEmpty()) return false; - - // We cannot compare the ByteStrings directly, because they are different objects - String childParentId = childSpan.get().getParentSpanId().toStringUtf8(); - String parentId = parentSpan.get().getSpanId().toStringUtf8(); - return childParentId.equals(parentId); - })); + List spans = getSpans(traces); + + Optional parentSpan = findSpan(spans, parentSpanName); + Optional childSpan = findSpan(spans, childSpanName); + boolean spansExist = parentSpan.isPresent() && childSpan.isPresent(); + + assertTrue(spansExist); + + // We cannot compare the ByteStrings directly, because they are different objects + String parentId = parentSpan.get().getSpanId().toStringUtf8(); + String childParentId = childSpan.get().getParentSpanId().toStringUtf8(); + String parentTraceId = parentSpan.get().getTraceId().toStringUtf8(); + String childTraceId = childSpan.get().getTraceId().toStringUtf8(); + + assertEquals(parentId, childParentId); + assertEquals(parentTraceId, childTraceId); } /** - * This method asserts that no spans with the given names exist. + * This method asserts that no spans from inspectIT with the given names exist. * * @param traces the collection of traces * @param parentSpanName the name of the parent span @@ -88,33 +82,68 @@ private void assertSpans( */ private void assertNoSpans( Collection traces, String parentSpanName, String childSpanName) { - Stream> spanLists = getSpanLists(traces); + List spans = getInspectItSpans(traces); - assertTrue( - spanLists.anyMatch( - spans -> { - Optional parentSpan = findSpan(spans, parentSpanName); - Optional childSpan = findSpan(spans, childSpanName); + Optional parentSpan = findSpan(spans, parentSpanName); + Optional childSpan = findSpan(spans, childSpanName); + boolean noSpanExist = parentSpan.isEmpty() && childSpan.isEmpty(); - return parentSpan.isEmpty() && childSpan.isEmpty(); - })); + assertTrue(noSpanExist); + } + + /** + * Maps the provided traces to a collection of spans. + * + * @param traces the collection of traces + * @return the collection of span within the provided traces + */ + private List getSpans(Collection traces) { + Predicate scopeFilter = (scopeName) -> true; + return getSpans(traces, scopeFilter); } /** - * Maps the provided traces to a collection of span lists. + * Maps the provided traces to a collection of spans, which were created by inspectIT. * * @param traces the collection of traces - * @return the collection of span lists within the provided traces + * @return the collection of span within the provided traces */ - private Stream> getSpanLists(Collection traces) { + private List getInspectItSpans(Collection traces) { + Predicate scopeFilter = (scopeName) -> scopeName.equals(INSTRUMENTATION_SCOPE_NAME); + return getSpans(traces, scopeFilter); + } + + /** + * Maps the provided traces to a collection of spans filtered by a specific instrumentation scope. + * + * @param traces the collection of traces + * @param scopeFilter the filter for the instrumentation scope + * @return the filtered collection of span within the provided traces + */ + private List getSpans( + Collection traces, Predicate scopeFilter) { return traces.stream() .flatMap( trace -> trace.getResourceSpansList().stream() .flatMap( resourceSpans -> - resourceSpans.getScopeSpansList().stream() - .map(ScopeSpans::getSpansList))); + filterAndExtractSpans(resourceSpans.getScopeSpansList(), scopeFilter))) + .toList(); + } + + /** + * Filters and extracts all {@link Span}s from {@link ScopeSpans}. + * + * @param scopeSpansList the list of {@link ScopeSpans} from {@link ResourceSpans} + * @param scopeFilter the filter for the instrumentation scope + * @return the collection of filtered spans as stream + */ + private Stream filterAndExtractSpans( + List scopeSpansList, Predicate scopeFilter) { + return scopeSpansList.stream() + .filter(scopeSpans -> scopeFilter.test(scopeSpans.getScope().getName())) + .flatMap(scopeSpans -> scopeSpans.getSpansList().stream()); } /**