From b8517ecd4b2be814e62918fcf867b990e6eaaf51 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sun, 7 Jun 2026 12:45:19 +0200 Subject: [PATCH 01/27] Initialize go project --- go.mod | 5 +++++ go.sum | 2 ++ build.gradle => old/build.gradle | 0 .../code-analysis}/checkstyle-suppression.xml | 0 {code-analysis => old/code-analysis}/checkstyle.xml | 0 .../code-analysis}/code-analysis.gradle | 0 {code-analysis => old/code-analysis}/pmd.xml | 0 {code-analysis => old/code-analysis}/pre-commit | 0 gradle.properties => old/gradle.properties | 0 {gradle => old/gradle}/wrapper/gradle-wrapper.jar | Bin .../gradle}/wrapper/gradle-wrapper.properties | 0 gradlew => old/gradlew | 0 gradlew.bat => old/gradlew.bat | 0 settings.gradle => old/settings.gradle | 0 .../net/explorviz/span/kafka/KafkaTestResource.java | 0 {src => old/src}/main/avro/ExplorvizProtocol.avdl | 0 {src => old/src}/main/avro/SpanData.avdl | 0 {src => old/src}/main/docker/Dockerfile.jvm | 0 {src => old/src}/main/docker/Dockerfile.legacy-jar | 0 {src => old/src}/main/docker/Dockerfile.native | 0 .../src}/main/docker/Dockerfile.native-micro | 0 .../trace/adapter/conversion/TopologyProducer.kt | 0 .../injection/ReadOnlyKeyValueStoreProducer.kt | 0 .../explorviz/trace/adapter/service/TokenService.kt | 0 .../adapter/service/converter/AttributesReader.kt | 0 .../service/converter/DefaultAttributeValues.kt | 0 .../trace/adapter/service/converter/IdHelper.kt | 0 .../adapter/service/converter/SpanConverter.kt | 0 .../adapter/service/converter/SpanConverterImpl.kt | 0 .../adapter/service/converter/fqn/DefaultParser.kt | 0 .../adapter/service/converter/fqn/FqnParser.kt | 0 .../adapter/service/converter/fqn/JavaFqnParser.kt | 0 .../service/validation/DefaultSpanValidator.kt | 0 .../adapter/service/validation/SpanValidator.kt | 0 .../explorviz/trace/application/StartupLogger.java | 0 .../trace/kafka/IgnoreRecordTooLargeHandler.java | 0 .../trace/kafka/SchemaRegistryClientProducer.java | 0 .../net/explorviz/trace/kafka/SerdeProducer.java | 0 .../net/explorviz/trace/kafka/ShutdownHandler.java | 0 .../trace/messaging/KafkaSpanExporter.java | 0 .../trace/persistence/PersistenceSpan.java | 0 .../trace/persistence/PersistenceSpanProcessor.java | 0 .../src}/main/resources/application.properties | 0 .../src}/main/resources/explorviz-banner.txt | 0 .../trace/adapter/conversion/TopologyTest.kt | 0 .../injection/MockSchemaRegistryClientProducer.kt | 0 .../adapter/injection/MockSerdeStructureProducer.kt | 0 .../injection/MockSerdeTokenEventProducer.kt | 0 .../service/converter/AttributesReaderTest.kt | 0 .../service/converter/DefaultAttributeValueTest.kt | 0 .../service/converter/SpanConverterImplTest.java | 0 .../service/validation/DefaultSpanValidatorTest.kt | 0 .../trace/adapter/translation/IdHelperTest.kt | 0 {.github/workflows => workflows}/ci.yaml | 0 .../workflows => workflows}/release-version.yml | 0 55 files changed, 7 insertions(+) create mode 100644 go.mod create mode 100644 go.sum rename build.gradle => old/build.gradle (100%) rename {code-analysis => old/code-analysis}/checkstyle-suppression.xml (100%) rename {code-analysis => old/code-analysis}/checkstyle.xml (100%) rename {code-analysis => old/code-analysis}/code-analysis.gradle (100%) rename {code-analysis => old/code-analysis}/pmd.xml (100%) rename {code-analysis => old/code-analysis}/pre-commit (100%) rename gradle.properties => old/gradle.properties (100%) rename {gradle => old/gradle}/wrapper/gradle-wrapper.jar (100%) rename {gradle => old/gradle}/wrapper/gradle-wrapper.properties (100%) rename gradlew => old/gradlew (100%) rename gradlew.bat => old/gradlew.bat (100%) rename settings.gradle => old/settings.gradle (100%) rename {src => old/src}/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java (100%) rename {src => old/src}/main/avro/ExplorvizProtocol.avdl (100%) rename {src => old/src}/main/avro/SpanData.avdl (100%) rename {src => old/src}/main/docker/Dockerfile.jvm (100%) rename {src => old/src}/main/docker/Dockerfile.legacy-jar (100%) rename {src => old/src}/main/docker/Dockerfile.native (100%) rename {src => old/src}/main/docker/Dockerfile.native-micro (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/TokenService.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt (100%) rename {src => old/src}/main/java/net/explorviz/trace/application/StartupLogger.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/kafka/SerdeProducer.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/kafka/ShutdownHandler.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/persistence/PersistenceSpan.java (100%) rename {src => old/src}/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java (100%) rename {src => old/src}/main/resources/application.properties (100%) rename {src => old/src}/main/resources/explorviz-banner.txt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt (100%) rename {src => old/src}/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt (100%) rename {.github/workflows => workflows}/ci.yaml (100%) rename {.github/workflows => workflows}/release-version.yml (100%) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b258313 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/ExplorViz/trace-service + +go 1.25.10 + +require github.com/twmb/franz-go v1.21.3 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9c374ab --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/twmb/franz-go v1.21.3 h1:q9Mo8ri+OwBQBjKqrerNCqNWJlJnUDe2qnYsj2V3hdI= +github.com/twmb/franz-go v1.21.3/go.mod h1:rfoMTnVk7107fhTGxfEKIHP/e7tPe6oyij/ywzO0czk= diff --git a/build.gradle b/old/build.gradle similarity index 100% rename from build.gradle rename to old/build.gradle diff --git a/code-analysis/checkstyle-suppression.xml b/old/code-analysis/checkstyle-suppression.xml similarity index 100% rename from code-analysis/checkstyle-suppression.xml rename to old/code-analysis/checkstyle-suppression.xml diff --git a/code-analysis/checkstyle.xml b/old/code-analysis/checkstyle.xml similarity index 100% rename from code-analysis/checkstyle.xml rename to old/code-analysis/checkstyle.xml diff --git a/code-analysis/code-analysis.gradle b/old/code-analysis/code-analysis.gradle similarity index 100% rename from code-analysis/code-analysis.gradle rename to old/code-analysis/code-analysis.gradle diff --git a/code-analysis/pmd.xml b/old/code-analysis/pmd.xml similarity index 100% rename from code-analysis/pmd.xml rename to old/code-analysis/pmd.xml diff --git a/code-analysis/pre-commit b/old/code-analysis/pre-commit similarity index 100% rename from code-analysis/pre-commit rename to old/code-analysis/pre-commit diff --git a/gradle.properties b/old/gradle.properties similarity index 100% rename from gradle.properties rename to old/gradle.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/old/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to old/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/old/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to old/gradle/wrapper/gradle-wrapper.properties diff --git a/gradlew b/old/gradlew similarity index 100% rename from gradlew rename to old/gradlew diff --git a/gradlew.bat b/old/gradlew.bat similarity index 100% rename from gradlew.bat rename to old/gradlew.bat diff --git a/settings.gradle b/old/settings.gradle similarity index 100% rename from settings.gradle rename to old/settings.gradle diff --git a/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java b/old/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java similarity index 100% rename from src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java rename to old/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java diff --git a/src/main/avro/ExplorvizProtocol.avdl b/old/src/main/avro/ExplorvizProtocol.avdl similarity index 100% rename from src/main/avro/ExplorvizProtocol.avdl rename to old/src/main/avro/ExplorvizProtocol.avdl diff --git a/src/main/avro/SpanData.avdl b/old/src/main/avro/SpanData.avdl similarity index 100% rename from src/main/avro/SpanData.avdl rename to old/src/main/avro/SpanData.avdl diff --git a/src/main/docker/Dockerfile.jvm b/old/src/main/docker/Dockerfile.jvm similarity index 100% rename from src/main/docker/Dockerfile.jvm rename to old/src/main/docker/Dockerfile.jvm diff --git a/src/main/docker/Dockerfile.legacy-jar b/old/src/main/docker/Dockerfile.legacy-jar similarity index 100% rename from src/main/docker/Dockerfile.legacy-jar rename to old/src/main/docker/Dockerfile.legacy-jar diff --git a/src/main/docker/Dockerfile.native b/old/src/main/docker/Dockerfile.native similarity index 100% rename from src/main/docker/Dockerfile.native rename to old/src/main/docker/Dockerfile.native diff --git a/src/main/docker/Dockerfile.native-micro b/old/src/main/docker/Dockerfile.native-micro similarity index 100% rename from src/main/docker/Dockerfile.native-micro rename to old/src/main/docker/Dockerfile.native-micro diff --git a/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt b/old/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt rename to old/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt diff --git a/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt b/old/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt rename to old/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt b/old/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/TokenService.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt b/old/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt diff --git a/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt b/old/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt similarity index 100% rename from src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt rename to old/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt diff --git a/src/main/java/net/explorviz/trace/application/StartupLogger.java b/old/src/main/java/net/explorviz/trace/application/StartupLogger.java similarity index 100% rename from src/main/java/net/explorviz/trace/application/StartupLogger.java rename to old/src/main/java/net/explorviz/trace/application/StartupLogger.java diff --git a/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java b/old/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java similarity index 100% rename from src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java rename to old/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java diff --git a/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java b/old/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java similarity index 100% rename from src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java rename to old/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java diff --git a/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java b/old/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java similarity index 100% rename from src/main/java/net/explorviz/trace/kafka/SerdeProducer.java rename to old/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java diff --git a/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java b/old/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java similarity index 100% rename from src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java rename to old/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java diff --git a/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java b/old/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java similarity index 100% rename from src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java rename to old/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java diff --git a/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java b/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java similarity index 100% rename from src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java rename to old/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java diff --git a/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java b/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java similarity index 100% rename from src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java rename to old/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java diff --git a/src/main/resources/application.properties b/old/src/main/resources/application.properties similarity index 100% rename from src/main/resources/application.properties rename to old/src/main/resources/application.properties diff --git a/src/main/resources/explorviz-banner.txt b/old/src/main/resources/explorviz-banner.txt similarity index 100% rename from src/main/resources/explorviz-banner.txt rename to old/src/main/resources/explorviz-banner.txt diff --git a/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt b/old/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt rename to old/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt diff --git a/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt b/old/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt rename to old/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt diff --git a/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt b/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt rename to old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt diff --git a/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt b/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt rename to old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt diff --git a/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt b/old/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt rename to old/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt diff --git a/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt b/old/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt rename to old/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt diff --git a/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java b/old/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java rename to old/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java diff --git a/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt b/old/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt rename to old/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt diff --git a/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt b/old/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt similarity index 100% rename from src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt rename to old/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt diff --git a/.github/workflows/ci.yaml b/workflows/ci.yaml similarity index 100% rename from .github/workflows/ci.yaml rename to workflows/ci.yaml diff --git a/.github/workflows/release-version.yml b/workflows/release-version.yml similarity index 100% rename from .github/workflows/release-version.yml rename to workflows/release-version.yml From 160acb5dc7e1154a53252b1f1e2fedfbbe36b9dd Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Wed, 10 Jun 2026 19:08:27 +0200 Subject: [PATCH 02/27] WIP: go rewrite --- cmd/trace-service/main.go | 87 +++++++++++++++++++++++++ go.mod | 22 ++++++- go.sum | 62 ++++++++++++++++++ internal/conversion/parsing/code.go | 47 ++++++++++++++ internal/conversion/parsing/common.go | 49 ++++++++++++++ internal/conversion/parsing/doc.go | 8 +++ internal/conversion/parsing/fqn.go | 92 +++++++++++++++++++++++++++ internal/conversion/span.go | 83 ++++++++++++++++++++++++ 8 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 cmd/trace-service/main.go create mode 100644 internal/conversion/parsing/code.go create mode 100644 internal/conversion/parsing/common.go create mode 100644 internal/conversion/parsing/doc.go create mode 100644 internal/conversion/parsing/fqn.go create mode 100644 internal/conversion/span.go diff --git a/cmd/trace-service/main.go b/cmd/trace-service/main.go new file mode 100644 index 0000000..06e3a40 --- /dev/null +++ b/cmd/trace-service/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "flag" + "log/slog" + "os" + "strings" + + "github.com/ExplorViz/trace-service/internal/conversion" + "github.com/ExplorViz/trace-service/internal/conversion/parsing" + + "github.com/twmb/franz-go/pkg/kgo" + + "google.golang.org/protobuf/proto" + + coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" + commonpb "go.opentelemetry.io/proto/otlp/common/v1" + resourcepb "go.opentelemetry.io/proto/otlp/resource/v1" + tracepb "go.opentelemetry.io/proto/otlp/trace/v1" +) + +type SpanRequest struct { + Span *tracepb.Span + Scope *commonpb.InstrumentationScope + Resource *resourcepb.Resource +} + +var ( + seedBrokers = flag.String("brokers", "localhost:9092", "comma delimited list of seed brokers") + topic = flag.String("topic", "test", "topic to produce to and consume from") + registry = flag.String("registry", "localhost:8081", "schema registry port to talk to") +) + +func main() { + // TODO schema registry + + // rcl, err := sr.NewClient(sr.URLs(*registry)) + // if err != nil { + // slog.Error("unable to create schema registry client", "error", err) + // os.Exit(1) + // } + // ss, err := rcl.CreateSchema(context.Background(), *topic+"-value", sr.Schema{ + // Schema: schemaText, + // Type: sr.TypeProtobuf, + // }) + // slog.Info("created or reusing schema", "subject", ss.Subject, "version", ss.Version, "id", ss.ID) + + cl, err := kgo.NewClient( + kgo.SeedBrokers(strings.Split(*seedBrokers, ",")...), + kgo.DefaultProduceTopic(*topic), + kgo.ConsumeTopics(*topic), + ) + if err != nil { + slog.Error("unable to initialize kgo client", "error", err) + os.Exit(1) + } + + spans := make(chan SpanRequest) + + for { + fs := cl.PollFetches(context.Background()) + fs.EachRecord(func(r *kgo.Record) { + var req coltracepb.ExportTraceServiceRequest + + if err := proto.Unmarshal(r.Value, &req); err != nil { + slog.Debug("invalid protocol buffer", "error", err) + return + } + + for _, rs := range req.ResourceSpans { + for _, ss := range rs.ScopeSpans { + for _, s := range ss.Spans { + spans <- SpanRequest{s, ss.Scope, rs.Resource} + } + } + } + }) + } +} + +func worker(spans <-chan SpanRequest, cl *kgo.Client) { + for req := range spans { + s := parsing.NewSpanReader(req.Span, req.Scope, req.Resource) + conversion.ConvertSpan(s) + } +} diff --git a/go.mod b/go.mod index b258313..31a231e 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,24 @@ module github.com/ExplorViz/trace-service go 1.25.10 -require github.com/twmb/franz-go v1.21.3 // indirect +require ( + github.com/twmb/franz-go v1.21.3 + github.com/twmb/franz-go/pkg/sr v1.7.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/proto/otlp v1.10.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.2 // indirect +) diff --git a/go.sum b/go.sum index 9c374ab..416be26 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,64 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twmb/franz-go v1.21.3 h1:q9Mo8ri+OwBQBjKqrerNCqNWJlJnUDe2qnYsj2V3hdI= github.com/twmb/franz-go v1.21.3/go.mod h1:rfoMTnVk7107fhTGxfEKIHP/e7tPe6oyij/ywzO0czk= +github.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI= +github.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= +github.com/twmb/franz-go/pkg/sr v1.7.0 h1:wHStlO6aOPWWgZ68ZYcdtQe9tRbkcTc1gRLbgs+8QAA= +github.com/twmb/franz-go/pkg/sr v1.7.0/go.mod h1:64CsHlsQnyFRq1sYPcCmlRrEG3PlLPb6cDddx2wGr28= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/conversion/parsing/code.go b/internal/conversion/parsing/code.go new file mode 100644 index 0000000..b7ca48b --- /dev/null +++ b/internal/conversion/parsing/code.go @@ -0,0 +1,47 @@ +package parsing + +import ( + "cmp" + "errors" + "fmt" + + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" +) + +type CodeSpanEntity struct { + FilePath string + FuncName string + ClassName string + Language string + GitCommitHash string +} + +func (c CodeSpanEntity) Id() string { + return c.FilePath + " " + c.FuncName + " " + c.ClassName + " " + c.GitCommitHash +} + +func ParseCodeSpan(s SpanReader) (SpanEntity, error) { + fqn := s.SpanAttribute(semconv.CodeFunctionNameKey).GetStringValue() + lang := s.ResourceAttribute(semconv.TelemetrySDKLanguageKey).GetStringValue() + + parsedFqn := ParseFunctionFqn(fqn, lang) + + if parsedFqn.FuncName == "" { + return &CodeSpanEntity{}, errors.New("code parser: function name could not be extracted") + } + + filePath := cmp.Or(parsedFqn.FilePath, s.SpanAttribute(semconv.CodeFilePathKey).GetStringValue()) + + if filePath == "" { + return CodeSpanEntity{}, fmt.Errorf("code parser: file path could not be extracted from FQN and %s not given", semconv.CodeFilePathKey) + } + + gitHash := s.SpanAttribute(semconv.VCSRefHeadRevisionKey).GetStringValue() + + return CodeSpanEntity{ + FilePath: filePath, + FuncName: parsedFqn.FuncName, + ClassName: parsedFqn.ClassName, + Language: lang, + GitCommitHash: gitHash}, nil +} diff --git a/internal/conversion/parsing/common.go b/internal/conversion/parsing/common.go new file mode 100644 index 0000000..7d70def --- /dev/null +++ b/internal/conversion/parsing/common.go @@ -0,0 +1,49 @@ +package parsing + +import ( + "go.opentelemetry.io/otel/attribute" + + commonpb "go.opentelemetry.io/proto/otlp/common/v1" + resourcepb "go.opentelemetry.io/proto/otlp/resource/v1" + tracepb "go.opentelemetry.io/proto/otlp/trace/v1" +) + +type SpanReader struct { + Span *tracepb.Span + Scope *commonpb.InstrumentationScope + Resource *resourcepb.Resource + + spanAttributes map[string]*commonpb.AnyValue + scopeAttributes map[string]*commonpb.AnyValue + resourceAttributes map[string]*commonpb.AnyValue +} + +func (s SpanReader) SpanAttribute(key attribute.Key) *commonpb.AnyValue { + return s.spanAttributes[string(key)] +} + +func (s SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { + return s.resourceAttributes[string(key)] +} + +func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resourcepb.Resource) SpanReader { + return SpanReader{ + spanAttributes: attrsToMap(s.Attributes), + scopeAttributes: attrsToMap(sc.Attributes), + resourceAttributes: attrsToMap(rs.Attributes), + } +} + +type SpanParser func(s SpanReader) (SpanEntity, error) + +type SpanEntity interface { + Id() string +} + +func attrsToMap(attrs []*commonpb.KeyValue) map[string]*commonpb.AnyValue { + m := make(map[string]*commonpb.AnyValue, len(attrs)) + for _, kv := range attrs { + m[kv.GetKey()] = kv.GetValue() + } + return m +} diff --git a/internal/conversion/parsing/doc.go b/internal/conversion/parsing/doc.go new file mode 100644 index 0000000..0ad8740 --- /dev/null +++ b/internal/conversion/parsing/doc.go @@ -0,0 +1,8 @@ +// Package parsing is concerned with the interpretation of span attributes, +// with the goal of infering the entity within the system described by the span. +// +// Various parsing functions and result types are defined which rely on different +// attributes from the [Otel semantic conventions]. +// +// [OTel semantic conventions]: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/ +package parsing diff --git a/internal/conversion/parsing/fqn.go b/internal/conversion/parsing/fqn.go new file mode 100644 index 0000000..89a200b --- /dev/null +++ b/internal/conversion/parsing/fqn.go @@ -0,0 +1,92 @@ +package parsing + +import ( + "log/slog" + "strings" + "unicode" +) + +type FqnParseResult struct { + FilePath string + FuncName string + ClassName string +} + +func ParseFunctionFqn(fqn string, language string) FqnParseResult { + switch strings.ToLower(language) { + case "java": + return parseJavaFqn(fqn) + default: + return parseGenericFqn(fqn) + } +} + +// ParseJavaFqn parses a Java method fully-qualified name (fqn) as specified by the [OTel semantic conventions]. +// Attempts to detect inner classes if multiple fqn segments are capitalized. +// +// Example fqn: +// +// "net.explorviz.app.MyConverter.MyInnerClass.convert" +// +// Example result: +// +// ["net/explorviz/app/MyConverter.java", "convert", "MyConverter.MyInnerClass"] +// +// [OTel semantic conventions]: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/ +func parseJavaFqn(fqn string) FqnParseResult { + const FileExtension = ".java" + + // Ignore lambda portion. If we explicitly want to support this, the data model would need to allow functions + // to have child functions. For now, we simplify and treat this as a call of the containing function. + fqnNoLambda := strings.SplitN(fqn, "$", 2)[0] + separatedFqn := strings.Split(fqnNoLambda, ".") + + lastPackageIndex := -1 + for i := len(separatedFqn) - 2; i >= 0; i-- { + if !startsWithUpper(separatedFqn[i]) { + lastPackageIndex = i + break + } + } + + fileNameIndex := lastPackageIndex + 1 + filePath := strings.Join(separatedFqn[:fileNameIndex+1], "/") + FileExtension + + className := "" + if fileNameIndex < len(separatedFqn)-1 { + className = strings.Join( + separatedFqn[fileNameIndex:len(separatedFqn)-1], + ".", + ) + } + + methodName := separatedFqn[len(separatedFqn)-1] + + return FqnParseResult{FilePath: filePath, FuncName: methodName, ClassName: className} +} + +// ParseGenericFqn acts as a fallback generic parser for fully-qualified function names where the language is not known. +// Assumes dot-separated string, where the last segment represents the function name and the prior segments represent the +// file's path. Does not add any file extension to the file path. Assumes no classes are included in the fqn. +// +// Example fqn: +// +// "src.main.SomeFile.myFunc" +// +// Example result: +// +// ["src/main/SomeFile", "myFunc", ""] +func parseGenericFqn(fqn string) FqnParseResult { + slog.Warn("unknown or missing SDK language, using generic fqn parser") + separatedFqn := strings.Split(fqn, ".") + return FqnParseResult{ + FilePath: strings.Join(separatedFqn[:len(separatedFqn)-1], "/"), + FuncName: separatedFqn[len(separatedFqn)-1]} +} + +func startsWithUpper(s string) bool { + for _, r := range s { + return unicode.IsUpper(r) + } + return false +} diff --git a/internal/conversion/span.go b/internal/conversion/span.go new file mode 100644 index 0000000..916f7f5 --- /dev/null +++ b/internal/conversion/span.go @@ -0,0 +1,83 @@ +package conversion + +import ( + "errors" + "fmt" + "log/slog" + + "github.com/ExplorViz/trace-service/internal/conversion/parsing" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" +) + +type ExplorVizAttribute struct { + Key attribute.Key + DefaultValue string +} + +var ExplorVizAttributes = struct { + LandscapeTokenID ExplorVizAttribute + LandscapeTokenSecret ExplorVizAttribute + EntityId ExplorVizAttribute +}{ + LandscapeTokenID: ExplorVizAttribute{ + Key: "explorviz.token.id", + DefaultValue: "mytokenvalue", + }, + LandscapeTokenSecret: ExplorVizAttribute{ + Key: "explorviz.token.secret", + DefaultValue: "mytokenvalue", + }, + EntityId: ExplorVizAttribute{ + Key: "explorviz.entity.id", + DefaultValue: "unknown", + }, +} + +type PersistenceSpan struct { + LandscapeTokenId string + LandscapeTokenSecret string + + TraceId string + SpanId string + ParentSpanId string + + StartTime uint64 + EndTime uint64 + + ApplicationName string + Entity parsing.SpanEntity +} + +var parserChain = []parsing.SpanParser{ + parsing.ParseCodeSpan, +} + +func ConvertSpan(sr parsing.SpanReader) (PersistenceSpan, error) { + tokenId := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() + tokenSecret := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() + appName := sr.ResourceAttribute(semconv.ServiceNameKey).GetStringValue() + + for _, parser := range parserChain { + res, err := parser(sr) + if err != nil { + slog.Debug(fmt.Sprintf("parser failed: %s", err)) + continue + } + return PersistenceSpan{ + LandscapeTokenId: tokenId, + LandscapeTokenSecret: tokenSecret, + + TraceId: string(sr.Span.GetTraceId()), + SpanId: string(sr.Span.GetSpanId()), + ParentSpanId: string(sr.Span.GetParentSpanId()), + + StartTime: sr.Span.GetStartTimeUnixNano(), + EndTime: sr.Span.GetEndTimeUnixNano(), + + ApplicationName: appName, + Entity: res, + }, nil + } + return PersistenceSpan{}, errors.New("no matching span parser found") +} From a662a6d865c3739b7bcd4b137029af3e60f0ffb9 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 11 Jun 2026 11:08:25 +0200 Subject: [PATCH 03/27] Workers, protobuf definition --- cmd/trace-service/main.go | 30 ++++++++++++++++++++++++------ go.mod | 1 - go.sum | 2 -- proto/persistence_span.proto | 31 +++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 proto/persistence_span.proto diff --git a/cmd/trace-service/main.go b/cmd/trace-service/main.go index 06e3a40..dbaf6bf 100644 --- a/cmd/trace-service/main.go +++ b/cmd/trace-service/main.go @@ -3,8 +3,10 @@ package main import ( "context" "flag" + "fmt" "log/slog" "os" + "runtime" "strings" "github.com/ExplorViz/trace-service/internal/conversion" @@ -56,7 +58,13 @@ func main() { os.Exit(1) } - spans := make(chan SpanRequest) + spans := make(chan parsing.SpanReader) + results := make(chan conversion.PersistenceSpan) + + workerCount := runtime.NumCPU() + for range workerCount { + go spanWorker(spans, results, cl) + } for { fs := cl.PollFetches(context.Background()) @@ -71,7 +79,7 @@ func main() { for _, rs := range req.ResourceSpans { for _, ss := range rs.ScopeSpans { for _, s := range ss.Spans { - spans <- SpanRequest{s, ss.Scope, rs.Resource} + spans <- parsing.NewSpanReader(s, ss.Scope, rs.Resource) } } } @@ -79,9 +87,19 @@ func main() { } } -func worker(spans <-chan SpanRequest, cl *kgo.Client) { - for req := range spans { - s := parsing.NewSpanReader(req.Span, req.Scope, req.Resource) - conversion.ConvertSpan(s) +func spanWorker(spans <-chan parsing.SpanReader, results chan<- conversion.PersistenceSpan, cl *kgo.Client) { + for s := range spans { + p, err := conversion.ConvertSpan(s) + if err != nil { + slog.Error(fmt.Sprintf("failed to convert span: %s", err)) + continue + } + results <- p + } +} + +func producerWorker(results <-chan conversion.PersistenceSpan, cl *kgo.Client) { + for p := range results { + } } diff --git a/go.mod b/go.mod index 31a231e..4c8b265 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.25.10 require ( github.com/twmb/franz-go v1.21.3 - github.com/twmb/franz-go/pkg/sr v1.7.0 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/proto/otlp v1.10.0 google.golang.org/protobuf v1.36.11 diff --git a/go.sum b/go.sum index 416be26..919e655 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ github.com/twmb/franz-go v1.21.3 h1:q9Mo8ri+OwBQBjKqrerNCqNWJlJnUDe2qnYsj2V3hdI= github.com/twmb/franz-go v1.21.3/go.mod h1:rfoMTnVk7107fhTGxfEKIHP/e7tPe6oyij/ywzO0czk= github.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI= github.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= -github.com/twmb/franz-go/pkg/sr v1.7.0 h1:wHStlO6aOPWWgZ68ZYcdtQe9tRbkcTc1gRLbgs+8QAA= -github.com/twmb/franz-go/pkg/sr v1.7.0/go.mod h1:64CsHlsQnyFRq1sYPcCmlRrEG3PlLPb6cDddx2wGr28= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= diff --git a/proto/persistence_span.proto b/proto/persistence_span.proto new file mode 100644 index 0000000..6a51c0e --- /dev/null +++ b/proto/persistence_span.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +option go_package = "github.com/ExplorViz/trace-service/internal/proto/spanpb"; + +message SpanData { + string landscape_token_id = 1; + string landscape_token_secret = 2; + + string trace_id = 3; + string span_id = 4; + string span_name = 5; + optional string parent_id = 6; + + uint64 start_time = 7; + uint64 end_time = 8; + + string application_name = 9; + + oneof entity_descriptor { + CodeDescriptor code_descriptor = 10; + } +} + +message CodeDescriptor { + string id = 1; + string file_path = 2; + string function_name = 3; + optional string class_name = 4; + optional string language = 5; + optional string git_commit_hash = 6; +} \ No newline at end of file From 25e35601757df2a5f0b1c319ea3fd1de996fa423 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 11 Jun 2026 19:08:38 +0200 Subject: [PATCH 04/27] protobuf conversion, produce to out topic, flag parsing --- cmd/trace-service/main.go | 138 ++++++-- internal/conversion/parsing/doc.go | 4 +- internal/conversion/parsing/fqn.go | 2 +- internal/conversion/span.go | 4 +- .../genproto/spanpb/persistence_span.pb.go | 331 ++++++++++++++++++ proto/persistence_span.proto | 2 +- 6 files changed, 440 insertions(+), 41 deletions(-) create mode 100644 internal/genproto/spanpb/persistence_span.pb.go diff --git a/cmd/trace-service/main.go b/cmd/trace-service/main.go index dbaf6bf..1aa5583 100644 --- a/cmd/trace-service/main.go +++ b/cmd/trace-service/main.go @@ -6,65 +6,86 @@ import ( "fmt" "log/slog" "os" + "os/signal" "runtime" - "strings" + "sync/atomic" + "time" "github.com/ExplorViz/trace-service/internal/conversion" "github.com/ExplorViz/trace-service/internal/conversion/parsing" + "github.com/ExplorViz/trace-service/internal/genproto/spanpb" "github.com/twmb/franz-go/pkg/kgo" "google.golang.org/protobuf/proto" coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" - commonpb "go.opentelemetry.io/proto/otlp/common/v1" - resourcepb "go.opentelemetry.io/proto/otlp/resource/v1" - tracepb "go.opentelemetry.io/proto/otlp/trace/v1" ) -type SpanRequest struct { - Span *tracepb.Span - Scope *commonpb.InstrumentationScope - Resource *resourcepb.Resource -} - var ( - seedBrokers = flag.String("brokers", "localhost:9092", "comma delimited list of seed brokers") - topic = flag.String("topic", "test", "topic to produce to and consume from") - registry = flag.String("registry", "localhost:8081", "schema registry port to talk to") + seedBroker = flag.String("broker", "localhost:9092", "network endpoint of the Kafka broker to use (:)") + inTopic = flag.String("topic-in", "test", "Kafka topic to consume OpenTelemetry spans from") + outTopic = flag.String("topic-out", "explorviz.spans.out", "Kafka topic to produce parsed spans into") + logInterval = flag.Duration("log-interval", time.Second, "interval at which received spans should be logged (e.g. \"5s\")") + + lastReceivedSpans atomic.Int64 ) func main() { - // TODO schema registry - - // rcl, err := sr.NewClient(sr.URLs(*registry)) - // if err != nil { - // slog.Error("unable to create schema registry client", "error", err) - // os.Exit(1) - // } - // ss, err := rcl.CreateSchema(context.Background(), *topic+"-value", sr.Schema{ - // Schema: schemaText, - // Type: sr.TypeProtobuf, - // }) - // slog.Info("created or reusing schema", "subject", ss.Subject, "version", ss.Version, "id", ss.ID) + flag.Parse() cl, err := kgo.NewClient( - kgo.SeedBrokers(strings.Split(*seedBrokers, ",")...), - kgo.DefaultProduceTopic(*topic), - kgo.ConsumeTopics(*topic), + kgo.SeedBrokers(*seedBroker), + kgo.DefaultProduceTopic(*outTopic), + kgo.ConsumeTopics(*inTopic), ) if err != nil { slog.Error("unable to initialize kgo client", "error", err) os.Exit(1) } + defer cl.Close() + + sigs := make(chan os.Signal, 2) + signal.Notify(sigs, os.Interrupt) + go func() { + <-sigs + fmt.Println("received interrupt signal; closing client") + cl.Close() + <-sigs + fmt.Println("received second interrupt; exiting") + os.Exit(1) + }() + + fmt.Print(` + ______ _ __ ___ + | ____| | | \ \ / (_) + | |__ __ ___ __ | | ___ _ _\ \ / / _ ____ + | __| \ \/ / '_ \| |/ _ \| '__\ \/ / | |_ / + | |____ > <| |_) | | (_) | | \ / | |/ / + |______/_/\_\ .__/|_|\___/|_| \/ |_/___| + | | + |_| trace-service + +`) + + go func() { + ticker := time.NewTicker(*logInterval) + defer ticker.Stop() + + for range ticker.C { + count := lastReceivedSpans.Swap(0) + slog.Info(fmt.Sprintf("received %d spans", count)) + } + }() spans := make(chan parsing.SpanReader) - results := make(chan conversion.PersistenceSpan) + results := make(chan *spanpb.SpanData) workerCount := runtime.NumCPU() for range workerCount { - go spanWorker(spans, results, cl) + go spanWorker(spans, results) } + go producerWorker(results, cl) for { fs := cl.PollFetches(context.Background()) @@ -87,19 +108,66 @@ func main() { } } -func spanWorker(spans <-chan parsing.SpanReader, results chan<- conversion.PersistenceSpan, cl *kgo.Client) { +func spanWorker(spans <-chan parsing.SpanReader, results chan<- *spanpb.SpanData) { for s := range spans { p, err := conversion.ConvertSpan(s) if err != nil { - slog.Error(fmt.Sprintf("failed to convert span: %s", err)) + slog.Error("failed to convert span", "error", err) continue } - results <- p + results <- toProto(p) } } -func producerWorker(results <-chan conversion.PersistenceSpan, cl *kgo.Client) { - for p := range results { +func producerWorker(results <-chan *spanpb.SpanData, cl *kgo.Client) { + for s := range results { + out, err := proto.Marshal(s) + if err != nil { + slog.Error("failed to encode protobuf", "error", err) + continue + } + cl.ProduceSync(context.Background(), &kgo.Record{ + Key: []byte(s.LandscapeTokenId), + Value: out, + }) + } +} + +func toProto(ps conversion.PersistenceSpan) *spanpb.SpanData { + s := spanpb.SpanData{ + LandscapeTokenId: ps.LandscapeTokenId, + LandscapeTokenSecret: ps.LandscapeTokenSecret, + + TraceId: ps.TraceId, + SpanId: ps.SpanId, + SpanName: ps.SpanName, + ParentId: strOrNil(ps.ParentSpanId), + + StartTime: ps.StartTime, + EndTime: ps.EndTime, + + ApplicationName: ps.ApplicationName, + } + + switch e := ps.Entity.(type) { + case parsing.CodeSpanEntity: + s.EntityDescriptor = &spanpb.SpanData_CodeDescriptor{ + CodeDescriptor: &spanpb.CodeDescriptor{ + FilePath: e.FilePath, + FunctionName: e.FuncName, + ClassName: strOrNil(e.ClassName), + Language: strOrNil(e.Language), + GitCommitHash: strOrNil(e.GitCommitHash), + }, + } + } + + return &s +} +func strOrNil(s string) *string { + if s == "" { + return nil } + return &s } diff --git a/internal/conversion/parsing/doc.go b/internal/conversion/parsing/doc.go index 0ad8740..72a81c6 100644 --- a/internal/conversion/parsing/doc.go +++ b/internal/conversion/parsing/doc.go @@ -1,8 +1,8 @@ // Package parsing is concerned with the interpretation of span attributes, -// with the goal of infering the entity within the system described by the span. +// with the goal of infering the entity within the system that the span describes. // // Various parsing functions and result types are defined which rely on different -// attributes from the [Otel semantic conventions]. +// attributes from the [OTel semantic conventions]. // // [OTel semantic conventions]: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/ package parsing diff --git a/internal/conversion/parsing/fqn.go b/internal/conversion/parsing/fqn.go index 89a200b..64f79a2 100644 --- a/internal/conversion/parsing/fqn.go +++ b/internal/conversion/parsing/fqn.go @@ -17,6 +17,7 @@ func ParseFunctionFqn(fqn string, language string) FqnParseResult { case "java": return parseJavaFqn(fqn) default: + slog.Warn("unknown or missing SDK language, using generic fqn parser", "telemetry.sdk.language", language) return parseGenericFqn(fqn) } } @@ -77,7 +78,6 @@ func parseJavaFqn(fqn string) FqnParseResult { // // ["src/main/SomeFile", "myFunc", ""] func parseGenericFqn(fqn string) FqnParseResult { - slog.Warn("unknown or missing SDK language, using generic fqn parser") separatedFqn := strings.Split(fqn, ".") return FqnParseResult{ FilePath: strings.Join(separatedFqn[:len(separatedFqn)-1], "/"), diff --git a/internal/conversion/span.go b/internal/conversion/span.go index 916f7f5..dbcb382 100644 --- a/internal/conversion/span.go +++ b/internal/conversion/span.go @@ -2,7 +2,6 @@ package conversion import ( "errors" - "fmt" "log/slog" "github.com/ExplorViz/trace-service/internal/conversion/parsing" @@ -40,6 +39,7 @@ type PersistenceSpan struct { TraceId string SpanId string + SpanName string ParentSpanId string StartTime uint64 @@ -61,7 +61,7 @@ func ConvertSpan(sr parsing.SpanReader) (PersistenceSpan, error) { for _, parser := range parserChain { res, err := parser(sr) if err != nil { - slog.Debug(fmt.Sprintf("parser failed: %s", err)) + slog.Debug("parser failed", "error", err) continue } return PersistenceSpan{ diff --git a/internal/genproto/spanpb/persistence_span.pb.go b/internal/genproto/spanpb/persistence_span.pb.go new file mode 100644 index 0000000..7d5a164 --- /dev/null +++ b/internal/genproto/spanpb/persistence_span.pb.go @@ -0,0 +1,331 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.19.6 +// source: persistence_span.proto + +package spanpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SpanData struct { + state protoimpl.MessageState `protogen:"open.v1"` + LandscapeTokenId string `protobuf:"bytes,1,opt,name=landscape_token_id,json=landscapeTokenId,proto3" json:"landscape_token_id,omitempty"` + LandscapeTokenSecret string `protobuf:"bytes,2,opt,name=landscape_token_secret,json=landscapeTokenSecret,proto3" json:"landscape_token_secret,omitempty"` + TraceId string `protobuf:"bytes,3,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + SpanId string `protobuf:"bytes,4,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` + SpanName string `protobuf:"bytes,5,opt,name=span_name,json=spanName,proto3" json:"span_name,omitempty"` + ParentId *string `protobuf:"bytes,6,opt,name=parent_id,json=parentId,proto3,oneof" json:"parent_id,omitempty"` + StartTime uint64 `protobuf:"varint,7,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime uint64 `protobuf:"varint,8,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + ApplicationName string `protobuf:"bytes,9,opt,name=application_name,json=applicationName,proto3" json:"application_name,omitempty"` + // Types that are valid to be assigned to EntityDescriptor: + // + // *SpanData_CodeDescriptor + EntityDescriptor isSpanData_EntityDescriptor `protobuf_oneof:"entity_descriptor"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SpanData) Reset() { + *x = SpanData{} + mi := &file_persistence_span_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SpanData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SpanData) ProtoMessage() {} + +func (x *SpanData) ProtoReflect() protoreflect.Message { + mi := &file_persistence_span_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SpanData.ProtoReflect.Descriptor instead. +func (*SpanData) Descriptor() ([]byte, []int) { + return file_persistence_span_proto_rawDescGZIP(), []int{0} +} + +func (x *SpanData) GetLandscapeTokenId() string { + if x != nil { + return x.LandscapeTokenId + } + return "" +} + +func (x *SpanData) GetLandscapeTokenSecret() string { + if x != nil { + return x.LandscapeTokenSecret + } + return "" +} + +func (x *SpanData) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +func (x *SpanData) GetSpanId() string { + if x != nil { + return x.SpanId + } + return "" +} + +func (x *SpanData) GetSpanName() string { + if x != nil { + return x.SpanName + } + return "" +} + +func (x *SpanData) GetParentId() string { + if x != nil && x.ParentId != nil { + return *x.ParentId + } + return "" +} + +func (x *SpanData) GetStartTime() uint64 { + if x != nil { + return x.StartTime + } + return 0 +} + +func (x *SpanData) GetEndTime() uint64 { + if x != nil { + return x.EndTime + } + return 0 +} + +func (x *SpanData) GetApplicationName() string { + if x != nil { + return x.ApplicationName + } + return "" +} + +func (x *SpanData) GetEntityDescriptor() isSpanData_EntityDescriptor { + if x != nil { + return x.EntityDescriptor + } + return nil +} + +func (x *SpanData) GetCodeDescriptor() *CodeDescriptor { + if x != nil { + if x, ok := x.EntityDescriptor.(*SpanData_CodeDescriptor); ok { + return x.CodeDescriptor + } + } + return nil +} + +type isSpanData_EntityDescriptor interface { + isSpanData_EntityDescriptor() +} + +type SpanData_CodeDescriptor struct { + CodeDescriptor *CodeDescriptor `protobuf:"bytes,10,opt,name=code_descriptor,json=codeDescriptor,proto3,oneof"` +} + +func (*SpanData_CodeDescriptor) isSpanData_EntityDescriptor() {} + +type CodeDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + FunctionName string `protobuf:"bytes,3,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` + ClassName *string `protobuf:"bytes,4,opt,name=class_name,json=className,proto3,oneof" json:"class_name,omitempty"` + Language *string `protobuf:"bytes,5,opt,name=language,proto3,oneof" json:"language,omitempty"` + GitCommitHash *string `protobuf:"bytes,6,opt,name=git_commit_hash,json=gitCommitHash,proto3,oneof" json:"git_commit_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodeDescriptor) Reset() { + *x = CodeDescriptor{} + mi := &file_persistence_span_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodeDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodeDescriptor) ProtoMessage() {} + +func (x *CodeDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_persistence_span_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodeDescriptor.ProtoReflect.Descriptor instead. +func (*CodeDescriptor) Descriptor() ([]byte, []int) { + return file_persistence_span_proto_rawDescGZIP(), []int{1} +} + +func (x *CodeDescriptor) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *CodeDescriptor) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *CodeDescriptor) GetFunctionName() string { + if x != nil { + return x.FunctionName + } + return "" +} + +func (x *CodeDescriptor) GetClassName() string { + if x != nil && x.ClassName != nil { + return *x.ClassName + } + return "" +} + +func (x *CodeDescriptor) GetLanguage() string { + if x != nil && x.Language != nil { + return *x.Language + } + return "" +} + +func (x *CodeDescriptor) GetGitCommitHash() string { + if x != nil && x.GitCommitHash != nil { + return *x.GitCommitHash + } + return "" +} + +var File_persistence_span_proto protoreflect.FileDescriptor + +const file_persistence_span_proto_rawDesc = "" + + "\n" + + "\x16persistence_span.proto\"\xa5\x03\n" + + "\bSpanData\x12,\n" + + "\x12landscape_token_id\x18\x01 \x01(\tR\x10landscapeTokenId\x124\n" + + "\x16landscape_token_secret\x18\x02 \x01(\tR\x14landscapeTokenSecret\x12\x19\n" + + "\btrace_id\x18\x03 \x01(\tR\atraceId\x12\x17\n" + + "\aspan_id\x18\x04 \x01(\tR\x06spanId\x12\x1b\n" + + "\tspan_name\x18\x05 \x01(\tR\bspanName\x12 \n" + + "\tparent_id\x18\x06 \x01(\tH\x01R\bparentId\x88\x01\x01\x12\x1d\n" + + "\n" + + "start_time\x18\a \x01(\x04R\tstartTime\x12\x19\n" + + "\bend_time\x18\b \x01(\x04R\aendTime\x12)\n" + + "\x10application_name\x18\t \x01(\tR\x0fapplicationName\x12:\n" + + "\x0fcode_descriptor\x18\n" + + " \x01(\v2\x0f.CodeDescriptorH\x00R\x0ecodeDescriptorB\x13\n" + + "\x11entity_descriptorB\f\n" + + "\n" + + "_parent_id\"\x84\x02\n" + + "\x0eCodeDescriptor\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1b\n" + + "\tfile_path\x18\x02 \x01(\tR\bfilePath\x12#\n" + + "\rfunction_name\x18\x03 \x01(\tR\ffunctionName\x12\"\n" + + "\n" + + "class_name\x18\x04 \x01(\tH\x00R\tclassName\x88\x01\x01\x12\x1f\n" + + "\blanguage\x18\x05 \x01(\tH\x01R\blanguage\x88\x01\x01\x12+\n" + + "\x0fgit_commit_hash\x18\x06 \x01(\tH\x02R\rgitCommitHash\x88\x01\x01B\r\n" + + "\v_class_nameB\v\n" + + "\t_languageB\x12\n" + + "\x10_git_commit_hashB=Z;github.com/ExplorViz/trace-service/internal/genproto/spanpbb\x06proto3" + +var ( + file_persistence_span_proto_rawDescOnce sync.Once + file_persistence_span_proto_rawDescData []byte +) + +func file_persistence_span_proto_rawDescGZIP() []byte { + file_persistence_span_proto_rawDescOnce.Do(func() { + file_persistence_span_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_persistence_span_proto_rawDesc), len(file_persistence_span_proto_rawDesc))) + }) + return file_persistence_span_proto_rawDescData +} + +var file_persistence_span_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_persistence_span_proto_goTypes = []any{ + (*SpanData)(nil), // 0: SpanData + (*CodeDescriptor)(nil), // 1: CodeDescriptor +} +var file_persistence_span_proto_depIdxs = []int32{ + 1, // 0: SpanData.code_descriptor:type_name -> CodeDescriptor + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_persistence_span_proto_init() } +func file_persistence_span_proto_init() { + if File_persistence_span_proto != nil { + return + } + file_persistence_span_proto_msgTypes[0].OneofWrappers = []any{ + (*SpanData_CodeDescriptor)(nil), + } + file_persistence_span_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_persistence_span_proto_rawDesc), len(file_persistence_span_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_persistence_span_proto_goTypes, + DependencyIndexes: file_persistence_span_proto_depIdxs, + MessageInfos: file_persistence_span_proto_msgTypes, + }.Build() + File_persistence_span_proto = out.File + file_persistence_span_proto_goTypes = nil + file_persistence_span_proto_depIdxs = nil +} diff --git a/proto/persistence_span.proto b/proto/persistence_span.proto index 6a51c0e..562ab34 100644 --- a/proto/persistence_span.proto +++ b/proto/persistence_span.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option go_package = "github.com/ExplorViz/trace-service/internal/proto/spanpb"; +option go_package = "github.com/ExplorViz/trace-service/internal/genproto/spanpb"; message SpanData { string landscape_token_id = 1; From d601b1c63ad6820e55517706752704376e191776 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sun, 14 Jun 2026 19:37:20 +0200 Subject: [PATCH 05/27] Remove "old" directory --- old/build.gradle | 200 --------- old/code-analysis/checkstyle-suppression.xml | 15 - old/code-analysis/checkstyle.xml | 381 ------------------ old/code-analysis/code-analysis.gradle | 30 -- old/code-analysis/pmd.xml | 97 ----- old/code-analysis/pre-commit | 12 - old/gradle.properties | 13 - old/gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 0 bytes old/gradle/wrapper/gradle-wrapper.properties | 5 - old/gradlew | 185 --------- old/gradlew.bat | 104 ----- old/settings.gradle | 11 - .../span/kafka/KafkaTestResource.java | 43 -- old/src/main/avro/ExplorvizProtocol.avdl | 49 --- old/src/main/avro/SpanData.avdl | 17 - old/src/main/docker/Dockerfile.jvm | 97 ----- old/src/main/docker/Dockerfile.legacy-jar | 93 ----- old/src/main/docker/Dockerfile.native | 27 -- old/src/main/docker/Dockerfile.native-micro | 30 -- .../adapter/conversion/TopologyProducer.kt | 137 ------- .../ReadOnlyKeyValueStoreProducer.kt | 43 -- .../trace/adapter/service/TokenService.kt | 34 -- .../service/converter/AttributesReader.kt | 115 ------ .../converter/DefaultAttributeValues.kt | 20 - .../adapter/service/converter/IdHelper.kt | 30 -- .../service/converter/SpanConverter.kt | 14 - .../service/converter/SpanConverterImpl.kt | 91 ----- .../service/converter/fqn/DefaultParser.kt | 16 - .../service/converter/fqn/FqnParser.kt | 19 - .../service/converter/fqn/JavaFqnParser.kt | 33 -- .../validation/DefaultSpanValidator.kt | 155 ------- .../service/validation/SpanValidator.kt | 15 - .../trace/application/StartupLogger.java | 24 -- .../kafka/IgnoreRecordTooLargeHandler.java | 33 -- .../kafka/SchemaRegistryClientProducer.java | 25 -- .../explorviz/trace/kafka/SerdeProducer.java | 35 -- .../trace/kafka/ShutdownHandler.java | 55 --- .../trace/messaging/KafkaSpanExporter.java | 48 --- .../trace/persistence/PersistenceSpan.java | 25 -- .../persistence/PersistenceSpanProcessor.java | 53 --- old/src/main/resources/application.properties | 78 ---- old/src/main/resources/explorviz-banner.txt | 11 - .../trace/adapter/conversion/TopologyTest.kt | 350 ---------------- .../MockSchemaRegistryClientProducer.kt | 17 - .../injection/MockSerdeStructureProducer.kt | 41 -- .../injection/MockSerdeTokenEventProducer.kt | 45 --- .../service/converter/AttributesReaderTest.kt | 290 ------------- .../converter/DefaultAttributeValueTest.kt | 48 --- .../converter/SpanConverterImplTest.java | 81 ---- .../validation/DefaultSpanValidatorTest.kt | 261 ------------ .../trace/adapter/translation/IdHelperTest.kt | 25 -- 51 files changed, 3676 deletions(-) delete mode 100644 old/build.gradle delete mode 100644 old/code-analysis/checkstyle-suppression.xml delete mode 100644 old/code-analysis/checkstyle.xml delete mode 100644 old/code-analysis/code-analysis.gradle delete mode 100644 old/code-analysis/pmd.xml delete mode 100755 old/code-analysis/pre-commit delete mode 100644 old/gradle.properties delete mode 100644 old/gradle/wrapper/gradle-wrapper.jar delete mode 100644 old/gradle/wrapper/gradle-wrapper.properties delete mode 100755 old/gradlew delete mode 100644 old/gradlew.bat delete mode 100644 old/settings.gradle delete mode 100644 old/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java delete mode 100644 old/src/main/avro/ExplorvizProtocol.avdl delete mode 100644 old/src/main/avro/SpanData.avdl delete mode 100644 old/src/main/docker/Dockerfile.jvm delete mode 100644 old/src/main/docker/Dockerfile.legacy-jar delete mode 100644 old/src/main/docker/Dockerfile.native delete mode 100644 old/src/main/docker/Dockerfile.native-micro delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt delete mode 100644 old/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt delete mode 100644 old/src/main/java/net/explorviz/trace/application/StartupLogger.java delete mode 100644 old/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java delete mode 100644 old/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java delete mode 100644 old/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java delete mode 100644 old/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java delete mode 100644 old/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java delete mode 100644 old/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java delete mode 100644 old/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java delete mode 100644 old/src/main/resources/application.properties delete mode 100644 old/src/main/resources/explorviz-banner.txt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt delete mode 100644 old/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt diff --git a/old/build.gradle b/old/build.gradle deleted file mode 100644 index ec5e38e..0000000 --- a/old/build.gradle +++ /dev/null @@ -1,200 +0,0 @@ -plugins { - id 'java' - id 'io.quarkus' - - id 'com.adarshr.test-logger' version '3.2.0' - id "org.kordamp.gradle.jandex" version "2.0.0" - - id "org.jetbrains.kotlin.jvm" version "2.0.21" - id "org.jetbrains.kotlin.plugin.allopen" version "2.0.21" - - id "io.gitlab.arturbosch.detekt" version "1.23.7" - id "com.diffplug.spotless" version "6.25.0" -} - -apply from: 'code-analysis/code-analysis.gradle' - -repositories { - mavenCentral() - mavenLocal() - maven { - url "https://packages.confluent.io/maven/" - } -} - -dependencies { - implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") - - implementation("io.quarkus:quarkus-vertx-http") - - implementation 'io.quarkus:quarkus-container-image-jib' - - implementation "io.quarkus:quarkus-kotlin" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - - // Needed for scheduled jobs - implementation 'io.quarkus:quarkus-scheduler' - - implementation "io.opentelemetry.proto:opentelemetry-proto:0.19.0-alpha" - - // Kafka Streams + Confluent Schema Registry + Avro (de)serializer - implementation 'io.quarkus:quarkus-kafka-streams' - implementation 'io.quarkus:quarkus-confluent-registry-avro' - implementation group: 'io.confluent', name: 'kafka-streams-avro-serde', version: "${confluentAvroSerdeVersion}" - implementation 'io.quarkus:quarkus-messaging-kafka' - - //implementation("org.jboss.slf4j:slf4j-jboss-logmanager") - //testImplementation("org.jboss.slf4j:slf4j-jboss-logmanager") - //integrationTestImplementation("org.jboss.slf4j:slf4j-jboss-logmanager") - - testImplementation 'io.quarkus:quarkus-junit5' - testImplementation 'io.quarkus:quarkus-junit5-mockito' - - // Integration / Api Tests - testImplementation "org.testcontainers:kafka:1.17.5" - testImplementation group: "org.apache.kafka", name: "kafka-streams-test-utils", version: "3.3.1" - integrationTestImplementation "org.testcontainers:kafka:1.17.5" - - // Integration Testing - integrationTestImplementation 'io.quarkus:quarkus-junit5' -} - -group 'net.explorviz' -version '1.0-SNAPSHOT' - -compileJava { - options.encoding = 'UTF-8' - options.compilerArgs << '-parameters' -} - -compileTestJava { - options.encoding = 'UTF-8' -} - -kotlin { - jvmToolchain(21) -} - -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -// Tasks Setups // - -sourceSets { - main { - java { - srcDirs 'build/classes/java/quarkus-generated-sources/grpc' - } - } - - integrationTest { - java.srcDir file('src/integrationTest/java') - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - } -} - -task integrationTest(type: Test) { - description = 'Runs integration tests.' - group = 'verification' - - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - shouldRunAfter test -} - -configurations { - integrationTestImplementation.extendsFrom implementation - integrationTestRuntimeOnly.extendsFrom runtimeOnly - - integrationTestImplementation.setCanBeResolved(true) - integrationTestRuntimeOnly.setCanBeResolved(true) -} - -// Regarding jandex dependency for each task -// https://github.com/kordamp/jandex-gradle-plugin/issues/9 - -tasks.named('imageBuild') { - dependsOn 'jandex' -} - -tasks.named('imagePush') { - dependsOn 'jandex' -} - -tasks.named('checkstyleMain') { - dependsOn 'jandex' -} - -tasks.named('pmdMain') { - dependsOn 'jandex' -} - -tasks.named('compileTestJava') { - dependsOn 'jandex' -} - -tasks.named('test') { - dependsOn 'jandex' -} - -tasks.named('compileIntegrationTestJava') { - dependsOn 'jandex' -} - -tasks.named('integrationTest') { - dependsOn 'jandex' -} - -tasks.named('quarkusDependenciesBuild') { - dependsOn 'jandex' -} - -spotless { - kotlin { - ktfmt().kotlinlangStyle().configure { - it.setMaxWidth(120) - } - } -} - - -tasks.withType(JavaCompile).configureEach { - // Turn on all javac warnings except classfile and processing, which produces many false-positives about annotations - options.compilerArgs << "-Xlint:all" << "-Xlint:-classfile" << "-Xlint:-processing" -} - -test { - useJUnitPlatform() - testLogging.showStandardStreams = true -} - -// Needed by Mockito since closed Kotlin classes cannot be mocked -allOpen { - annotation("jakarta.enterprise.context.ApplicationScoped") - annotation("jakarta.persistence.Entity") - annotation("io.quarkus.test.junit.QuarkusTest") -} - -integrationTest { - useJUnitPlatform() - testLogging.showStandardStreams = true -} - -def registerGitHook = { String taskName, String hookFile, String targetHook -> - tasks.register(taskName, Copy) { - from("code-analysis/${hookFile}") - into(".git/hooks") - rename { targetHook } - } -} - -registerGitHook("registerPreCommitHook", "pre-commit", "pre-commit") -registerGitHook("registerPreMergeCommitHook", "pre-commit", "pre-merge-commit") - -tasks.named("quarkusGenerateCode") { - dependsOn("registerPreCommitHook") - dependsOn("registerPreMergeCommitHook") -} diff --git a/old/code-analysis/checkstyle-suppression.xml b/old/code-analysis/checkstyle-suppression.xml deleted file mode 100644 index 8b5eea5..0000000 --- a/old/code-analysis/checkstyle-suppression.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/old/code-analysis/checkstyle.xml b/old/code-analysis/checkstyle.xml deleted file mode 100644 index dfc9f16..0000000 --- a/old/code-analysis/checkstyle.xml +++ /dev/null @@ -1,381 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/old/code-analysis/code-analysis.gradle b/old/code-analysis/code-analysis.gradle deleted file mode 100644 index 74b2b51..0000000 --- a/old/code-analysis/code-analysis.gradle +++ /dev/null @@ -1,30 +0,0 @@ -apply plugin: 'pmd' -apply plugin: 'checkstyle' -apply plugin: 'jacoco' - -jacocoTestReport { - - executionData { tasks.withType(Test).findAll { it.jacoco.destinationFile.exists() }*.jacoco.destinationFile } - - reports { - xml.required = true - html.required = true - } -} - -pmd { - // Empty ruleset is necessary for exclude-pattern - // https://stackoverflow.com/questions/32247190/pmd-exclude-pattern-with-gradle - ruleSets = [] - ruleSetFiles = files("code-analysis/pmd.xml") - ignoreFailures = false - toolVersion = "6.53.0" -} - -checkstyle { - configDirectory = file("code-analysis") - configFile = file("code-analysis/checkstyle.xml") - maxWarnings = 0 - ignoreFailures = false - toolVersion = "10.12.5" -} diff --git a/old/code-analysis/pmd.xml b/old/code-analysis/pmd.xml deleted file mode 100644 index 38ab891..0000000 --- a/old/code-analysis/pmd.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - CAU-SE PMD ruleset - - .*/io.opencensus/.* - .*/build/.* - .*/src/test/.* - .*/src/integrationTest/.* - .*/src/main/java/net/explorviz/span/hash/HighwayHash.java - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/old/code-analysis/pre-commit b/old/code-analysis/pre-commit deleted file mode 100755 index 4b4bf60..0000000 --- a/old/code-analysis/pre-commit +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -echo "Running pre-commit hook" - -./gradlew checkstyleMain pmdMain test -if [ $? -ne 0 ]; then - echo "Pre-commit hook failed" - exit 1 -fi - -echo "Pre-commit hook passed" -exit 0 diff --git a/old/gradle.properties b/old/gradle.properties deleted file mode 100644 index 8b32b51..0000000 --- a/old/gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -#Gradle properties -quarkusPluginVersion=3.21.2 -quarkusPlatformArtifactId=quarkus-bom -quarkusPluginId=io.quarkus -quarkusPlatformGroupId=io.quarkus.platform -quarkusPlatformVersion=3.21.2 -confluentAvroSerdeVersion=7.3.3 - -# Gradle daemon memory settings -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# Kotlin daemon memory settings -kotlin.daemon.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 diff --git a/old/gradle/wrapper/gradle-wrapper.jar b/old/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 62d4c053550b91381bbd28b1afc82d634bf73a8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58910 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 diff --git a/old/gradle/wrapper/gradle-wrapper.properties b/old/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a595206..0000000 --- a/old/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/old/gradlew b/old/gradlew deleted file mode 100755 index fbd7c51..0000000 --- a/old/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/old/gradlew.bat b/old/gradlew.bat deleted file mode 100644 index 5093609..0000000 --- a/old/gradlew.bat +++ /dev/null @@ -1,104 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/old/settings.gradle b/old/settings.gradle deleted file mode 100644 index 13f1c07..0000000 --- a/old/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - mavenLocal() - } - plugins { - id "${quarkusPluginId}" version "${quarkusPluginVersion}" - } -} -rootProject.name = 'trace-service' diff --git a/old/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java b/old/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java deleted file mode 100644 index 7991018..0000000 --- a/old/src/integrationTest/java/net/explorviz/span/kafka/KafkaTestResource.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.NewTopic; -import org.testcontainers.containers.KafkaContainer; -import org.testcontainers.utility.DockerImageName; - -public class KafkaTestResource implements QuarkusTestResourceLifecycleManager { - - private static final KafkaContainer KAFKA = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.3.0")); - - @Override - public Map start() { - KAFKA.start(); - final Properties config = new Properties(); - config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA.getBootstrapServers()); - - final AdminClient localKafkaAdmin = AdminClient.create(config); - - final int partitions = 1; - final short replication = 1; - final NewTopic topicSpans = new NewTopic("explorviz-spans-dynamic", partitions, replication); - final NewTopic topicTokens = new NewTopic("token-events", partitions, replication); - final List topics = Arrays.asList(topicSpans, topicTokens); - - localKafkaAdmin.createTopics(topics); - - return Collections.singletonMap("kafka.bootstrap.servers", KAFKA.getBootstrapServers()); - } - - @Override - public void stop() { - KAFKA.stop(); - } -} diff --git a/old/src/main/avro/ExplorvizProtocol.avdl b/old/src/main/avro/ExplorvizProtocol.avdl deleted file mode 100644 index d4c7724..0000000 --- a/old/src/main/avro/ExplorvizProtocol.avdl +++ /dev/null @@ -1,49 +0,0 @@ -@namespace("net.explorviz.avro") -protocol ExplorvizProtocol { - enum EventType { - CREATED, - DELETED, - ACCESS_GRANTED, - ACCESS_REVOKED, - CLONED - } - - record LandscapeToken { - string value; - string ownerId; - string secret; - long created; - string alias; - boolean isRequestedFromVSCodeExtension = false; - string projectName = ""; - string commitId = ""; - } - - record TokenEvent { - EventType type; - LandscapeToken token; - string clonedToken; - } - - record Span { - string landscapeToken; - string gitCommitChecksum = "cross-commit"; - string spanId; - string parentSpanId; - string traceId; - long startTimeEpochNano; - long endTimeEpochNano; - string fullyQualifiedOperationName; - string hostname; - string hostIpAddress; - string appName; - string appInstanceId; - string appLanguage; - string hashCode = "default-hashcode"; - - string k8sPodName = ""; - string k8sNodeName = ""; - string k8sNamespace = ""; - string k8sDeploymentName = ""; - } -} diff --git a/old/src/main/avro/SpanData.avdl b/old/src/main/avro/SpanData.avdl deleted file mode 100644 index cb89c3b..0000000 --- a/old/src/main/avro/SpanData.avdl +++ /dev/null @@ -1,17 +0,0 @@ -@namespace("net.explorviz.persistence.avro") -protocol SpanDataProtocol { - record SpanData { - string spanId; - string parentId; - string traceId; - string landscapeTokenId; - long startTime; - long endTime; - string applicationName; - string filePath; - string functionName; - string language; - union { null, string } className = null; - union { null, string } commitHash = null; - } -} diff --git a/old/src/main/docker/Dockerfile.jvm b/old/src/main/docker/Dockerfile.jvm deleted file mode 100644 index 79cb893..0000000 --- a/old/src/main/docker/Dockerfile.jvm +++ /dev/null @@ -1,97 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the container image run: -# -# ./gradlew build -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/explorviz.test-service-jvm . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-jvm -# -# If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. -# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 -# when running the container -# -# Then run the container using : -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-jvm -# -# This image uses the `run-java.sh` script to run the application. -# This scripts computes the command line to execute your Java application, and -# includes memory/GC tuning. -# You can configure the behavior using the following environment properties: -# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") -# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options -# in JAVA_OPTS (example: "-Dsome.property=foo") -# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is -# used to calculate a default maximal heap memory based on a containers restriction. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio -# of the container available memory as set here. The default is `50` which means 50% -# of the available memory is used as an upper boundary. You can skip this mechanism by -# setting this value to `0` in which case no `-Xmx` option is added. -# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This -# is used to calculate a default initial heap memory based on the maximum heap memory. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio -# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` -# is used as the initial heap size. You can skip this mechanism by setting this value -# to `0` in which case no `-Xms` option is added (example: "25") -# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. -# This is used to calculate the maximum value of the initial heap memory. If used in -# a container without any memory constraints for the container then this option has -# no effect. If there is a memory constraint then `-Xms` is limited to the value set -# here. The default is 4096MB which means the calculated value of `-Xms` never will -# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") -# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output -# when things are happening. This option, if set to true, will set -# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). -# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: -# true"). -# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). -# - CONTAINER_CORE_LIMIT: A calculated core limit as described in -# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") -# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). -# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. -# (example: "20") -# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. -# (example: "40") -# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. -# (example: "4") -# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus -# previous GC times. (example: "90") -# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") -# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") -# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should -# contain the necessary JRE command-line options to specify the required GC, which -# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). -# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") -# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") -# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be -# accessed directly. (example: "foo.example.com,bar.example.com") -# -### -FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 - -ENV LANGUAGE='en_US:en' - - -# We make four distinct layers so if there are application changes the library layers can be re-used -COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ -COPY --chown=185 build/quarkus-app/*.jar /deployments/ -COPY --chown=185 build/quarkus-app/app/ /deployments/app/ -COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ - -EXPOSE 8080 -USER 185 -ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" - -ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] - diff --git a/old/src/main/docker/Dockerfile.legacy-jar b/old/src/main/docker/Dockerfile.legacy-jar deleted file mode 100644 index 4d42dae..0000000 --- a/old/src/main/docker/Dockerfile.legacy-jar +++ /dev/null @@ -1,93 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.package.jar.type=legacy-jar -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/explorviz.test-service-legacy-jar . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-legacy-jar -# -# If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. -# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 -# when running the container -# -# Then run the container using : -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service-legacy-jar -# -# This image uses the `run-java.sh` script to run the application. -# This scripts computes the command line to execute your Java application, and -# includes memory/GC tuning. -# You can configure the behavior using the following environment properties: -# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") -# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options -# in JAVA_OPTS (example: "-Dsome.property=foo") -# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is -# used to calculate a default maximal heap memory based on a containers restriction. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio -# of the container available memory as set here. The default is `50` which means 50% -# of the available memory is used as an upper boundary. You can skip this mechanism by -# setting this value to `0` in which case no `-Xmx` option is added. -# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This -# is used to calculate a default initial heap memory based on the maximum heap memory. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio -# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` -# is used as the initial heap size. You can skip this mechanism by setting this value -# to `0` in which case no `-Xms` option is added (example: "25") -# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. -# This is used to calculate the maximum value of the initial heap memory. If used in -# a container without any memory constraints for the container then this option has -# no effect. If there is a memory constraint then `-Xms` is limited to the value set -# here. The default is 4096MB which means the calculated value of `-Xms` never will -# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") -# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output -# when things are happening. This option, if set to true, will set -# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). -# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: -# true"). -# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). -# - CONTAINER_CORE_LIMIT: A calculated core limit as described in -# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") -# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). -# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. -# (example: "20") -# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. -# (example: "40") -# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. -# (example: "4") -# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus -# previous GC times. (example: "90") -# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") -# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") -# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should -# contain the necessary JRE command-line options to specify the required GC, which -# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). -# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") -# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") -# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be -# accessed directly. (example: "foo.example.com,bar.example.com") -# -### -FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 - -ENV LANGUAGE='en_US:en' - - -COPY build/lib/* /deployments/lib/ -COPY build/*-runner.jar /deployments/quarkus-run.jar - -EXPOSE 8080 -USER 185 -ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" - -ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] diff --git a/old/src/main/docker/Dockerfile.native b/old/src/main/docker/Dockerfile.native deleted file mode 100644 index d83eaad..0000000 --- a/old/src/main/docker/Dockerfile.native +++ /dev/null @@ -1,27 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.native.enabled=true -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.native -t quarkus/explorviz.test-service . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service -# -### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root build/*-runner /work/application - -EXPOSE 8080 -USER 1001 - -ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/old/src/main/docker/Dockerfile.native-micro b/old/src/main/docker/Dockerfile.native-micro deleted file mode 100644 index e98a9cc..0000000 --- a/old/src/main/docker/Dockerfile.native-micro +++ /dev/null @@ -1,30 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. -# It uses a micro base image, tuned for Quarkus native executables. -# It reduces the size of the resulting container image. -# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.native.enabled=true -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/explorviz.test-service . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/explorviz.test-service -# -### -FROM quay.io/quarkus/quarkus-micro-image:2.0 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root build/*-runner /work/application - -EXPOSE 8080 -USER 1001 - -ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/old/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt b/old/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt deleted file mode 100644 index 711dd34..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/conversion/TopologyProducer.kt +++ /dev/null @@ -1,137 +0,0 @@ -package net.explorviz.trace.adapter.conversion - -import com.google.protobuf.InvalidProtocolBufferException -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest -import io.opentelemetry.proto.trace.v1.Span -import io.quarkus.scheduler.Scheduled -import jakarta.enterprise.context.ApplicationScoped -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import java.util.concurrent.atomic.AtomicInteger -import net.explorviz.trace.adapter.service.converter.SpanConverterImpl -import net.explorviz.avro.EventType -import net.explorviz.avro.TokenEvent -import net.explorviz.trace.adapter.service.validation.SpanValidator -import net.explorviz.trace.persistence.PersistenceSpan -import net.explorviz.trace.persistence.PersistenceSpanProcessor -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.common.utils.Bytes -import org.apache.kafka.streams.KeyValue -import org.apache.kafka.streams.StreamsBuilder -import org.apache.kafka.streams.Topology -import org.apache.kafka.streams.kstream.Consumed -import org.apache.kafka.streams.kstream.KStream -import org.apache.kafka.streams.kstream.Materialized -import org.apache.kafka.streams.kstream.Produced -import org.apache.kafka.streams.state.KeyValueStore -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.slf4j.Logger -import org.slf4j.LoggerFactory - - -/** Builds a KafkaStream topology instance with all its transformers. Entry point of the stream analysis. */ -@ApplicationScoped -class TopologyProducer { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(TopologyProducer::class.java) - } - - private val lastReceivedSpans = AtomicInteger(0) - private val lastInvalidSpans = AtomicInteger(0) - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in") lateinit var inTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.spans") lateinit var spansOutTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in.tokens") lateinit var tokensInTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.tokens-table") lateinit var tokensOutTopic: String - - @Inject lateinit var validator: SpanValidator - - @Inject lateinit var spanAvroSerde: SpecificAvroSerde - - @Inject lateinit var tokenEventAvroSerde: SpecificAvroSerde - - @Inject lateinit var spanConverter: SpanConverterImpl - - @Inject - lateinit var persistenceProcessor: PersistenceSpanProcessor - - - @Produces - fun buildTopology(): Topology { - val builder = StreamsBuilder() - - // BEGIN Conversion Stream - val spanByteStream: KStream = - builder.stream(inTopic, Consumed.with(Serdes.ByteArray(), Serdes.ByteArray())) - - val spanStream: KStream = - spanByteStream.flatMapValues { data -> - try { - val spanList = mutableListOf() - ExportTraceServiceRequest.parseFrom(data).resourceSpansList.forEach { resourceSpans -> - resourceSpans.scopeSpansList.forEach { scopeSpans -> spanList.addAll(scopeSpans.spansList) } - } - lastReceivedSpans.addAndGet(spanList.size) - spanList - } catch (e: InvalidProtocolBufferException) { - LOGGER.trace("Invalid protocol buffer: ${e.message}") - emptyList() - } - } - - // Validate Spans - val validSpanStream: KStream = - spanStream.flatMapValues { value -> - if (!validator.isValid(value)) { - lastInvalidSpans.incrementAndGet() - emptyList() - } else { - listOf(value) - } - } - - val persistenceStream: KStream = - validSpanStream.map { _, value -> - val span = spanConverter.fromOpenTelemetrySpan(value) - KeyValue(span.landscapeToken.toString(), span) - } - - persistenceStream.foreach { _ : String?, span: PersistenceSpan? -> - persistenceProcessor.accept( - span, - ) - } - // END Conversion Stream - - // BEGIN Token Stream - builder - .stream(tokensInTopic, Consumed.with(Serdes.String(), tokenEventAvroSerde)) - .filter { key, value -> - LOGGER.trace("Received token event for token value $key with event $value") - value == null || value.type == EventType.CREATED - } - .to(tokensOutTopic, Produced.with(Serdes.String(), tokenEventAvroSerde)) - - builder.globalTable( - tokensOutTopic, - Materialized.`as`>("token-events-global-store") - .withKeySerde(Serdes.String()) - .withValueSerde(tokenEventAvroSerde), - ) - // END Token Stream - - return builder.build() - } - - @Scheduled(every = "{explorviz.log.span.interval}") - fun logStatus() { - val spans = lastReceivedSpans.getAndSet(0) - val invalidSpans = lastInvalidSpans.getAndSet(0) - LOGGER.debug("Received $spans spans: ${spans - invalidSpans} valid, $invalidSpans invalid") - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt b/old/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt deleted file mode 100644 index 0080515..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/injection/ReadOnlyKeyValueStoreProducer.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.quarkus.arc.DefaultBean -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import net.explorviz.avro.TokenEvent -import org.apache.kafka.streams.KafkaStreams -import org.apache.kafka.streams.StoreQueryParameters -import org.apache.kafka.streams.errors.InvalidStateStoreException -import org.apache.kafka.streams.state.QueryableStoreTypes -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** Returns a {@link SchemaRegistryClient} that is used by this application with a maximum number of 10 schemas. */ -@Dependent -class ReadOnlyKeyValueStoreProducer { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(ReadOnlyKeyValueStoreProducer::class.java) - } - - @Inject lateinit var streams: KafkaStreams - - @Produces - @DefaultBean - fun getTokenEventStore(): ReadOnlyKeyValueStore { - while (true) { - try { - return streams.store( - StoreQueryParameters.fromNameAndType( - "token-events-global-store", - QueryableStoreTypes.keyValueStore(), - ), - ) - } catch (e: InvalidStateStoreException) { - LOGGER.debug("State store not yet initialized: ${e.message}") - LOGGER.debug("Will try again ...") - } - } - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt b/old/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt deleted file mode 100644 index 4f474d1..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/TokenService.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.explorviz.trace.adapter.service - -import jakarta.enterprise.context.ApplicationScoped -import jakarta.inject.Inject -import net.explorviz.avro.TokenEvent -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore - -/** Service to access available landscape tokens, backed by GlobalKTable and state store access. */ -@ApplicationScoped -class TokenService @Inject constructor(private val keyValueStore: ReadOnlyKeyValueStore) { - - /** - * Checks whether a given token exists. - * - * @param tokenValue the token to check - * @return `true` if the given token value is in the list of valid tokens - */ - fun validLandscapeTokenValue(tokenValue: String): Boolean { - val potentialEvent = keyValueStore[tokenValue] - return potentialEvent?.token?.value == tokenValue - } - - /** - * Checks whether a given landscape token and secret exist and belong to each other. - * - * @param tokenValue the token to check - * @param tokenSecret the secret to check - * @return `true` if the given token value is valid and the secret belongs to this token. - */ - fun validLandscapeTokenValueAndSecret(tokenValue: String, tokenSecret: String): Boolean { - val potentialEvent = keyValueStore[tokenValue] - return potentialEvent?.token?.value == tokenValue && potentialEvent.token.secret == tokenSecret - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt deleted file mode 100644 index 96d171f..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/AttributesReader.kt +++ /dev/null @@ -1,115 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.trace.v1.Span -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_APP_INSTANCE_ID -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_APP_LANG -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_APP_NAME -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_CLASS_FQN -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_FQN -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_GIT_COMMIT_CHECKSUM -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_HOST_IP -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_HOST_NAME -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_LANDSCAPE_SECRET -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN -import net.explorviz.trace.adapter.service.converter.DefaultAttributeValues.DEFAULT_PACKAGE_NAME -import org.apache.commons.lang3.StringUtils - -/** Reads the attributes of a [Span]. If an attribute is not found under the standard name, fallback attribute names - * may be provided. The preferred attribute names should always conform to the [OpenTelemetry Semantic Conventions]( - * https://opentelemetry.io/docs/specs/semconv/) - */ -open class AttributesReader(private val span: Span) { - - companion object { - const val LANDSCAPE_TOKEN = "explorviz.token.id" - const val TOKEN_SECRET = "explorviz.token.secret" - const val VCS_REF_HEAD_REVISION = "vcs.ref.head.revision" - const val GIT_COMMIT_CHECKSUM = "git_commit_checksum" - const val HOST_NAME = "host" - const val HOST_IP = "host_address" - const val APPLICATION_NAME = "service.name" - const val APPLICATION_INSTANCE_ID = "service.instance.id" - const val APPLICATION_LANGUAGE = "telemetry.sdk.language" - const val CODE_FILE_PATH = "code.file.path" - const val CODE_FUNCTION_NAME = "code.function.name" - const val CODE_FUNCTION = "code.function" // Deprecated semconv code attribute - const val CODE_NAMESPACE = "code.namespace" // Deprecated semconv code attribute - const val METHOD_FQN = "java.fqn" - const val K8S_POD_NAME = "k8s.pod.name" - const val K8S_NAMESPACE_NAME = "k8s.namespace.name" - const val K8S_NODE_NAME = "k8s.node.name" - const val K8S_DEPLOYMENT_NAME = "k8s.deployment.name" - } - - private val attributes: Map = span.attributesList.associate { it.key to it.value } - - open fun getAsString(key: String): String? { - return attributes[key]?.stringValue - } - - val landscapeToken: String - get() = getAsString(LANDSCAPE_TOKEN) ?: DEFAULT_LANDSCAPE_TOKEN - - val secret: String - get() = getAsString(TOKEN_SECRET) ?: DEFAULT_LANDSCAPE_SECRET - - val hostName: String - get() = getAsString(HOST_NAME) ?: DEFAULT_HOST_NAME - - val hostIpAddress: String - get() = getAsString(HOST_IP) ?: DEFAULT_HOST_IP - - val gitCommitChecksum: String - get() = getAsString(VCS_REF_HEAD_REVISION) ?: getAsString(GIT_COMMIT_CHECKSUM) ?: DEFAULT_GIT_COMMIT_CHECKSUM - - val applicationName: String - get() = getAsString(APPLICATION_NAME) ?: DEFAULT_APP_NAME - - val applicationInstanceId: String - get() = getAsString(APPLICATION_INSTANCE_ID) ?: DEFAULT_APP_INSTANCE_ID - - val applicationLanguage: String - get() = getAsString(APPLICATION_LANGUAGE) ?: DEFAULT_APP_LANG - - val filePath: String - get() = getAsString(CODE_FILE_PATH) ?: "" - - val methodFqn: String - get() { - val codeFunctionName = getAsString(CODE_FUNCTION_NAME) - val codeNamespace = getAsString(CODE_NAMESPACE) - val codeFunction = getAsString(CODE_FUNCTION) - val methodFqn = getAsString(METHOD_FQN) - - return codeFunctionName - ?: codeNamespace?.let { namespace -> codeFunction?.let { function -> "$namespace.$function" } } - ?: methodFqn - ?: generateMethodFqnFromSpanName() - } - - open fun generateMethodFqnFromSpanName(): String { - val spanName = span.name - if (spanName.isNullOrEmpty()) return DEFAULT_FQN - - val hierarchyDepth = StringUtils.countMatches(spanName, ".") - - return when { - hierarchyDepth == 0 -> "$DEFAULT_CLASS_FQN.$spanName" - hierarchyDepth == 1 -> "$DEFAULT_PACKAGE_NAME.$spanName" - else -> spanName // Assume span name contains fully qualified name - } - } - - val k8sPodName: String - get() = getAsString(K8S_POD_NAME) ?: "" - - val k8sNamespace: String - get() = getAsString(K8S_NAMESPACE_NAME) ?: "" - - val k8sNodeName: String - get() = getAsString(K8S_NODE_NAME) ?: "" - - val k8sDeploymentName: String - get() = getAsString(K8S_DEPLOYMENT_NAME) ?: "" -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt deleted file mode 100644 index b375386..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValues.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -/** Contains constants for missing attribute values of spans. */ -object DefaultAttributeValues { - const val DEFAULT_LANDSCAPE_TOKEN = "mytokenvalue" - const val DEFAULT_LANDSCAPE_SECRET = "mytokensecret" - const val DEFAULT_GIT_COMMIT_CHECKSUM = "UNKNOWN" // NOPMD - const val DEFAULT_HOST_IP = "0.0.0.0" // NOPMD - const val DEFAULT_HOST_NAME = "UNKNOWN-HOST" - const val DEFAULT_APP_NAME = "UNKNOWN-APPLICATION" - const val DEFAULT_APP_INSTANCE_ID = "0" - const val DEFAULT_APP_LANG = "UNKNOWN" - const val DEFAULT_FILE_PATH = "" - const val DEFAULT_PACKAGE_NAME = "unknownpkg" - const val DEFAULT_CLASS_NAME = "UnknownClass" - - const val DEFAULT_CLASS_FQN = "$DEFAULT_PACKAGE_NAME.$DEFAULT_CLASS_NAME" - const val DEFAULT_METHOD = "unknownMethod" - const val DEFAULT_FQN = "$DEFAULT_CLASS_FQN.$DEFAULT_METHOD" -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt deleted file mode 100644 index 9f97a6f..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/IdHelper.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import com.google.common.io.BaseEncoding - -/** Helper class to convert IDs given as byte arrays to strings. */ -object IdHelper { - - private const val SPAN_ID_LEN = 8 - private const val TRACE_ID_LEN = 16 - - /** - * Converts a span ID represented as a byte array to a readable string encoded in base 16. - * - * @param spanId the ID - * @return base16 encoding of the ID - */ - fun convertSpanId(spanId: ByteArray): String { - return BaseEncoding.base16().lowerCase().encode(spanId, 0, SPAN_ID_LEN) - } - - /** - * Converts a trace ID represented as a byte array to a readable string encoded in base 16. - * - * @param traceId the ID - * @return base16 encoding of the ID - */ - fun convertTraceId(traceId: ByteArray): String { - return BaseEncoding.base16().lowerCase().encode(traceId, 0, TRACE_ID_LEN) - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt deleted file mode 100644 index d174a57..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import io.opentelemetry.proto.trace.v1.Span - -/** - * Converts OpenTelemetry spans into another type. - * - * @param the type spans are converted to. - */ -interface SpanConverter { - - /** Converts an OpenTelemetry {@link Span} into {@link T}. */ - fun fromOpenTelemetrySpan(ocSpan: Span): T -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt deleted file mode 100644 index f88ddff..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/SpanConverterImpl.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import jakarta.enterprise.context.ApplicationScoped -import java.util.* -import net.explorviz.trace.adapter.service.converter.fqn.DefaultParser -import net.explorviz.trace.adapter.service.converter.fqn.FqnParser -import net.explorviz.trace.adapter.service.converter.fqn.JavaFqnParser -import net.explorviz.trace.persistence.PersistenceSpan -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** Converts a [io.opentelemetry.proto.trace.v1.Span] to a [PersistenceSpan]. */ -@ApplicationScoped -class SpanConverterImpl : SpanConverter { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(SpanConverterImpl::class.java) - - private val JAVA_PARSER = JavaFqnParser() - private val DEFAULT_PARSER = DefaultParser() - - private val PARSERS = mapOf( - "java" to JAVA_PARSER, - ) - - fun getFqnParser(language: String): FqnParser { - return PARSERS[language.lowercase()] ?: DEFAULT_PARSER - } - } - - override fun fromOpenTelemetrySpan(ocSpan: io.opentelemetry.proto.trace.v1.Span): PersistenceSpan { - - val attributesReader = AttributesReader(ocSpan) - - val gitCommitChecksum = attributesReader.gitCommitChecksum - val spanId = IdHelper.convertSpanId(ocSpan.spanId.toByteArray()) - val traceId = IdHelper.convertTraceId(ocSpan.traceId.toByteArray()) - val startTime = ocSpan.startTimeUnixNano - val endTime = ocSpan.endTimeUnixNano - val nodeIpAddress = attributesReader.hostIpAddress - val hostName = attributesReader.hostName - val applicationName = attributesReader.applicationName - val applicationLanguage = attributesReader.applicationLanguage - val applicationInstance = attributesReader.applicationInstanceId - val filePathAttrib = attributesReader.filePath - val methodFqn = attributesReader.methodFqn - val k8sPodName = attributesReader.k8sPodName - val k8sNodeName = attributesReader.k8sPodName - val k8sNamespace = attributesReader.k8sNamespace - val k8sDeploymentName = attributesReader.k8sDeploymentName - - val landscapeToken = attributesReader.landscapeToken; - - val parentSpanId = if (ocSpan.parentSpanId.size() > 0) { - IdHelper.convertSpanId(ocSpan.parentSpanId.toByteArray()) - } else { - "" - } - - val parsingResult: FqnParser.ParsingResult = getFqnParser(applicationLanguage).parseFunctionFqn(methodFqn) - - // Explicit file path attribute takes precedence. If unset, then parse from fqn - val filePath = - if (filePathAttrib != DefaultAttributeValues.DEFAULT_FILE_PATH) filePathAttrib else parsingResult.filePath - - val functionName = parsingResult.functionName - val className = parsingResult.className - - return PersistenceSpan( - landscapeToken, - gitCommitChecksum, - spanId, - parentSpanId, - traceId, - startTime, - endTime, - nodeIpAddress, - hostName, - applicationName, - applicationLanguage ?: PersistenceSpan.DEFAULT_LANGUAGE, - applicationInstance, - filePath, - functionName, - className, - k8sPodName, - k8sNodeName, - k8sNamespace, - k8sDeploymentName, - ) - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt deleted file mode 100644 index 8151279..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/DefaultParser.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.explorviz.trace.adapter.service.converter.fqn - -/** - * Fallback generic parser. Assumes dot-separated string, where the last segment represents the function name and - * the prior segments represent the file's fqn (e.g. "src/main/SomeFile/myFunc"). Does not add any file extension to - * the file fqn. Assumes no classes are included in the fqn. - */ -class DefaultParser : FqnParser { - - override fun parseFunctionFqn(functionFqn: String): FqnParser.ParsingResult { - val separatedFqn = functionFqn.split(".") - val fileFqn = separatedFqn.dropLast(1).joinToString(".") - val functionName = separatedFqn.last() - return FqnParser.ParsingResult(fileFqn, functionName); - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt deleted file mode 100644 index 79424a7..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/FqnParser.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.explorviz.trace.adapter.service.converter.fqn - -interface FqnParser { - - data class ParsingResult(val filePath: String, val functionName: String, val className: String? = null) - - /** - * Parses a function fqn according to some language specification and separates into: - * * the associated file's relative path as given from the fqn (separated by "/") The file path should crucially - * _include file extension_ if applicable. - * * the (unqualified) name of the function - * * the name of a class within the file, if included in the fqn, otherwise null. The name of any inner class is - * qualified against its parent classes (separated by "."). - * - * See [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/) for - * examples on how the fqn is structured for different languages. - */ - fun parseFunctionFqn(functionFqn: String): ParsingResult -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt b/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt deleted file mode 100644 index 94ebe2c..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/converter/fqn/JavaFqnParser.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.explorviz.trace.adapter.service.converter.fqn - -/** - * Parses Java method fqn as specified by - * [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/). - * - * Attempts to detect inner classes if multiple fqn segments are capitalized, e.g. - * `net.explorviz.app.MyClass.MyInnerClass.doSomething` would result in class name `MyClass.MyInnerClass`. - * - * Example fqn: `"net.explorviz.app.MyConverter.convert"` - * Example result: `["net/explorviz/app/MyConverter.java", "convert", "MyConverter"]` - */ -class JavaFqnParser : FqnParser { - companion object { - const val FILE_EXTENSION = ".java" - } - - override fun parseFunctionFqn(functionFqn: String): FqnParser.ParsingResult { - // Ignore lambda portion. If we explicitly want to support this, the data model would need to allow functions to - // have child functions. For now, we simplify and treat this as a call of the containing function. - val fqnWithoutLambda = functionFqn.substringBefore("$") - val separatedFqn = fqnWithoutLambda.split(".") - - val lastPackageIndex = separatedFqn.dropLast(1).indexOfLast { it.firstOrNull()?.isUpperCase() != true } - val fileNameIndex = lastPackageIndex + 1 - - val filePath = separatedFqn.take(fileNameIndex + 1).joinToString("/") + FILE_EXTENSION - val className = separatedFqn.subList(fileNameIndex, separatedFqn.lastIndex).joinToString(".") - val methodName = separatedFqn.last() - - return FqnParser.ParsingResult(filePath, methodName, className) - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt b/old/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt deleted file mode 100644 index 1a3f9c2..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidator.kt +++ /dev/null @@ -1,155 +0,0 @@ -package net.explorviz.trace.adapter.span.service.validation - -import io.opentelemetry.proto.trace.v1.Span -import jakarta.enterprise.context.ApplicationScoped -import jakarta.inject.Inject -import java.nio.file.Paths -import java.time.DateTimeException -import net.explorviz.trace.adapter.service.converter.AttributesReader -import net.explorviz.trace.adapter.service.TokenService -import net.explorviz.trace.adapter.service.validation.SpanValidator -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -@ApplicationScoped -class DefaultSpanValidator -@Inject constructor( - private val tokenService: TokenService, - @ConfigProperty(name = "explorviz.validate.token-existence") var validateTokens: Boolean = false -) : SpanValidator { - - companion object { - private val LOGGER: Logger = LoggerFactory.getLogger(DefaultSpanValidator::class.java) - private const val MIN_DEPTH_FQN_NAME = 2 // Method FQN must at least include class / file if no file path is set - } - - override fun isValid(span: Span): Boolean { - val attr = AttributesReader(span) - return validateTimestamp(span.startTimeUnixNano) && validateTimestamp(span.endTimeUnixNano) && isValid(attr) - } - - fun isValid(spanAttributes: AttributesReader): Boolean { - return validateToken( - spanAttributes.landscapeToken, - spanAttributes.secret, - ) && validateHost( - spanAttributes.hostName, - spanAttributes.hostIpAddress, - ) && validateApp(spanAttributes.applicationName, spanAttributes.applicationLanguage) && validateOperation( - spanAttributes.methodFqn, spanAttributes.filePath, - ) && validateK8s(spanAttributes) - } - - private fun validateToken(token: String?, givenSecret: String?): Boolean { - val hasTokenAndSecret = !token.isNullOrBlank() && !givenSecret.isNullOrBlank() - - if (token.isNullOrBlank()) { - LOGGER.trace("Invalid span: No or blank token.") - } - - if (givenSecret.isNullOrBlank()) { - LOGGER.trace("Invalid span: No or blank secret.") - } - - var isValid = true - if (hasTokenAndSecret && validateTokens) { - isValid = tokenService.validLandscapeTokenValueAndSecret(token!!, givenSecret!!) - if (!isValid) { - LOGGER.trace("Invalid span: Token and/or secret are unknown.") - } - } - - return hasTokenAndSecret && isValid - } - - private fun validateTimestamp(timestamp: Long): Boolean { - return try { - if (timestamp <= 0L) { - throw NumberFormatException("Time must be positive") - } - true - } catch (e: DateTimeException) { - LOGGER.trace("Invalid span timestamp: Date time exception - ${e.message}") - false - } catch (e: NumberFormatException) { - LOGGER.trace("Invalid span timestamp: Number format exception - ${e.message}") - false - } - } - - private fun validateHost(hostName: String?, hostIp: String?): Boolean { - val isValid = !hostName.isNullOrBlank() && !hostIp.isNullOrBlank() - - if (hostName.isNullOrBlank()) { - LOGGER.trace("Invalid span: No hostname.") - } - - if (hostIp.isNullOrBlank()) { - LOGGER.trace("Invalid span: No IP address.") - } - - return isValid - } - - private fun validateApp(appName: String?, appLang: String?): Boolean { - val isValid = !appName.isNullOrBlank() && !appLang.isNullOrBlank() - - if (appName.isNullOrBlank()) { - LOGGER.trace("Invalid span: No application name.") - } - - if (appLang.isNullOrBlank()) { - LOGGER.trace("Invalid span: No application language given.") - } - - return isValid - } - - private fun validateOperation(fqn: String, filePath: String): Boolean { - // Despite OTel semconv, absolute file paths are not desirable as we don't know where the application begins - val filePathIsValid = - filePath.isNotBlank() && runCatching { Paths.get(filePath).isAbsolute }.getOrDefault(false) - - val operationFqnSplit = fqn.split(".") - - if (filePathIsValid) { - if (operationFqnSplit.last().isBlank()) { - LOGGER.trace("Invalid span: Invalid operation name {}", fqn) - return false - } - return true - } else { - if (operationFqnSplit.size < MIN_DEPTH_FQN_NAME) { - LOGGER.trace( - "Invalid span: Operation name \"{}\" too short with invalid file path given: \"{}\"", - fqn, - filePath, - ) - return false - } - if (operationFqnSplit.any { it.isBlank() }) { - LOGGER.trace( - "Invalid span: Operation name \"{}\" contains empty segments with invalid file path given: \"{}\"", - fqn, - filePath, - ) - return false - } - } - - return true - } - - private fun validateK8s(spanAttributes: AttributesReader): Boolean { - val hasPodName = spanAttributes.k8sPodName.isNotEmpty() - val hasNamespace = spanAttributes.k8sNamespace.isNotEmpty() - val hasNodeName = spanAttributes.k8sNodeName.isNotEmpty() - val hasDeployment = spanAttributes.k8sDeploymentName.isNotEmpty() - - val hasAll = hasPodName && hasNamespace && hasNodeName && hasDeployment - val hasNone = !hasPodName && !hasNamespace && !hasNodeName && !hasDeployment - - return hasAll || hasNone - } -} diff --git a/old/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt b/old/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt deleted file mode 100644 index 81b9dd6..0000000 --- a/old/src/main/java/net/explorviz/trace/adapter/service/validation/SpanValidator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.explorviz.trace.adapter.service.validation - -import io.opentelemetry.proto.trace.v1.Span - -/** Validates [Span]s prior to processing. */ -interface SpanValidator { - - /** - * Checks if the given span is valid. - * - * @param span the span - * @return Boolean that indicates if span is valid. - */ - fun isValid(span: Span): Boolean -} diff --git a/old/src/main/java/net/explorviz/trace/application/StartupLogger.java b/old/src/main/java/net/explorviz/trace/application/StartupLogger.java deleted file mode 100644 index cfb180e..0000000 --- a/old/src/main/java/net/explorviz/trace/application/StartupLogger.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.explorviz.trace.application; - -import io.quarkus.runtime.StartupEvent; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@ApplicationScoped -public class StartupLogger { - - private static final Logger LOGGER = LoggerFactory.getLogger(StartupLogger.class); - - @ConfigProperty(name = "explorviz.span.api.timeverification.enabled") - /* default */ boolean isTimeVerificationEnabled; - - void onStart(@Observes final StartupEvent ev) { - LOGGER.atInfo().log("The application is starting..."); - - LOGGER.atInfo().addArgument(isTimeVerificationEnabled).log("API time ranges are enabled: {}"); - } - -} diff --git a/old/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java b/old/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java deleted file mode 100644 index c933127..0000000 --- a/old/src/main/java/net/explorviz/trace/kafka/IgnoreRecordTooLargeHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.quarkus.runtime.annotations.RegisterForReflection; -import java.util.Map; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.errors.RecordTooLargeException; -import org.apache.kafka.streams.errors.ErrorHandlerContext; -import org.apache.kafka.streams.errors.ProductionExceptionHandler; - -/** - * {@link ProductionExceptionHandler} used in the application.properties file. - * ({@link RecordTooLargeException}) lead to loss of records, but are not an unrecoverable error - * that should shut down the application. Therefore, we catch the exception, discard the record, and - * proceed. - */ -@RegisterForReflection -public class IgnoreRecordTooLargeHandler implements ProductionExceptionHandler { - - @Override - public void configure(final Map configs) { - // Nothing to do - } - - @Override - public ProductionExceptionHandlerResponse handle(final ErrorHandlerContext context, - final ProducerRecord record, final Exception exception) { - if (exception instanceof RecordTooLargeException) { - return ProductionExceptionHandlerResponse.CONTINUE; - } else { - return ProductionExceptionHandlerResponse.FAIL; - } - } -} diff --git a/old/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java b/old/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java deleted file mode 100644 index f0b585a..0000000 --- a/old/src/main/java/net/explorviz/trace/kafka/SchemaRegistryClientProducer.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.quarkus.arc.DefaultBean; -import jakarta.enterprise.inject.Produces; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -/** - * Returns a {@link SchemaRegistryClient} that is used by this application with a maximum number of - * 10 schemas. - */ -public class SchemaRegistryClientProducer { - - private static final int MAX_NUM_OF_SCHEMAS = 10; - - @ConfigProperty(name = "quarkus.kafka-streams.schema-registry-url") - /* default */ String schemaRegistryUrl; - - @Produces - @DefaultBean - public SchemaRegistryClient produceSchemaRegistryClient() { - return new CachedSchemaRegistryClient(this.schemaRegistryUrl, MAX_NUM_OF_SCHEMAS); - } -} diff --git a/old/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java b/old/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java deleted file mode 100644 index 3b643eb..0000000 --- a/old/src/main/java/net/explorviz/trace/kafka/SerdeProducer.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; -import io.quarkus.arc.DefaultBean; -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; -import java.util.Map; -import org.apache.avro.specific.SpecificRecord; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -/** - * Returns an injectable {@link SpecificAvroSerde}. - */ -@Dependent -public class SerdeProducer { - - @Inject - /* default */ SchemaRegistryClient registry; - - @ConfigProperty(name = "quarkus.kafka-streams.schema-registry-url") - /* default */ String schemaRegistryUrl; - - @Produces - @DefaultBean - public SpecificAvroSerde produceSpecificAvroSerde() { - final SpecificAvroSerde valueSerde = new SpecificAvroSerde<>(this.registry); - valueSerde.configure( - Map.of(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, this.schemaRegistryUrl), - false); - return valueSerde; - } -} diff --git a/old/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java b/old/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java deleted file mode 100644 index 0b91840..0000000 --- a/old/src/main/java/net/explorviz/trace/kafka/ShutdownHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.explorviz.trace.kafka; - -import io.quarkus.runtime.Quarkus; -import io.quarkus.runtime.ShutdownEvent; -import io.quarkus.runtime.StartupEvent; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.KafkaStreams.State; -import org.apache.kafka.streams.KafkaStreams.StateListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Registers a {@link StateListener} for the {@link KafkaStreams} object that is used as - * ErrorStateListener. The Quarkus application does not shut down automatically if the Kafka Streams - * thread goes into {@link State}.ERROR state. The Kafka streams ERROR state has no come back, - * therefore the application is dead. - */ -@ApplicationScoped -public class ShutdownHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(ShutdownHandler.class); - - @Inject - /* default */ KafkaStreams streams; - - /* default */ void onStart(@Observes final StartupEvent ev) { - this.streams.setStateListener(new ErrorStateListener()); - } - - /* default */ void onStop(@Observes final ShutdownEvent ev) { - // nothing to do - } - - private static class ErrorStateListener implements StateListener { - - @Override - public void onChange(final State newState, final State oldState) { - if (newState == State.ERROR) { - - LOGGER.atError().log("Kafka Streams thread died. Are Kafka topic initialized? " - + "Quarkus application will shut down."); - - LOGGER.atError().log("About to system exit due to Kafka Streams Error."); - - Quarkus.asyncExit(-1); - Quarkus.waitForExit(); - System.exit(-1); // NOPMD - } - - } - } -} diff --git a/old/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java b/old/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java deleted file mode 100644 index 379e7bf..0000000 --- a/old/src/main/java/net/explorviz/trace/messaging/KafkaSpanExporter.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.explorviz.trace.messaging; - -import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import net.explorviz.persistence.avro.SpanData; -import net.explorviz.trace.persistence.PersistenceSpan; -import org.eclipse.microprofile.reactive.messaging.Channel; -import org.eclipse.microprofile.reactive.messaging.Emitter; -import org.eclipse.microprofile.reactive.messaging.Message; - -@ApplicationScoped -public class KafkaSpanExporter { - - @Inject - @Channel("explorviz-spans") - Emitter emitter; - - public void persistSpan(PersistenceSpan span) { - SpanData spanData = mapToAvro(span); - - emitter.send( - Message.of(spanData) - .addMetadata( - OutgoingKafkaRecordMetadata.builder().withKey(span.traceId()).build())); - } - - private SpanData mapToAvro(PersistenceSpan span) { - return SpanData.newBuilder() - .setSpanId(span.spanId()) - .setParentId(span.parentSpanId()) - .setTraceId(span.traceId()) - .setLandscapeTokenId(span.landscapeToken().toString()) - .setStartTime(span.startTime()) - .setEndTime(span.endTime()) - .setApplicationName(span.applicationName()) - .setLanguage(span.applicationLanguage()) - .setFunctionName(span.functionName()) - .setFilePath(span.filePath()) - .setClassName( - span.className().isBlank() ? null : span.className()) - .setCommitHash( - span.gitCommitChecksum().isBlank() - ? null - : span.gitCommitChecksum()) - .build(); - } -} diff --git a/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java b/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java deleted file mode 100644 index aeb4ffa..0000000 --- a/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpan.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.explorviz.trace.persistence; - -public record PersistenceSpan( - String landscapeToken, - String gitCommitChecksum, - String spanId, - String parentSpanId, - String traceId, - long startTime, - long endTime, - String nodeIpAddress, // TODO: Convert into InetAddress type? - String hostName, - String applicationName, - String applicationLanguage, - String applicationInstance, - String filePath, - String functionName, - String className, - String k8sPodName, - String k8sNodeName, - String k8sNamespace, - String k8sDeploymentName -) { - public static final String DEFAULT_LANGUAGE = "LANGUAGE_UNSPECIFIED"; -} diff --git a/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java b/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java deleted file mode 100644 index 5fc16f4..0000000 --- a/old/src/main/java/net/explorviz/trace/persistence/PersistenceSpanProcessor.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.explorviz.trace.persistence; - -import io.quarkus.scheduler.Scheduled; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import net.explorviz.trace.messaging.KafkaSpanExporter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@ApplicationScoped -public class PersistenceSpanProcessor implements Consumer { - - private static final Logger LOGGER = LoggerFactory.getLogger(PersistenceSpanProcessor.class); - - private final AtomicLong lastProcessedSpans = new AtomicLong(0L); - private final AtomicLong lastExportedSpans = new AtomicLong(0L); - private final AtomicLong lastFailures = new AtomicLong(0L); - - private final ConcurrentMap> knownSpanIdsByLandscape = - new ConcurrentHashMap<>(1); - - @Inject - /* default */ KafkaSpanExporter exporter; - - @Override - public void accept(final PersistenceSpan span) { - final Set knownSpanIds = knownSpanIdsByLandscape.computeIfAbsent(span.landscapeToken(), - tokenValue -> ConcurrentHashMap.newKeySet()); - if (knownSpanIds.add(span.spanId())) { - exporter.persistSpan(span); - lastExportedSpans.incrementAndGet(); - } - - lastProcessedSpans.incrementAndGet(); - } - - @Scheduled(every = "{explorviz.log.span.interval}") - public void logStatus() { - final long processedSpans = this.lastProcessedSpans.getAndSet(0); - final long exportedSpans = this.lastExportedSpans.getAndSet(0); - LOGGER.atDebug().addArgument(processedSpans).addArgument(exportedSpans) - .log("Processed {} spans, exported {} spans."); - final long failures = this.lastFailures.getAndSet(0); - if (failures != 0) { - LOGGER.atWarn().addArgument(failures).log("Data loss occurred. {} span exports failed."); - } - } -} diff --git a/old/src/main/resources/application.properties b/old/src/main/resources/application.properties deleted file mode 100644 index ab20b48..0000000 --- a/old/src/main/resources/application.properties +++ /dev/null @@ -1,78 +0,0 @@ -quarkus.banner.path=explorviz-banner.txt -quarkus.devservices.enabled=false -cron.expr=*/10 * * * * ? - -########## -## HTTP ## -########## -%dev.quarkus.http.port=8083 -%dev.quarkus.http.host=0.0.0.0 -%dev.quarkus.http.cors=true -%dev.quarkus.http.cors.origins=/.*/ - -########### -## Kafka ## -########### -# necessary for mp.messaging -kafka.bootstrap.servers=kafka:9092 -%dev.kafka.bootstrap.servers=localhost:9091 -%test.kafka.bootstrap.servers=localhost:9091 - -################### -## Kafka Streams ## -################### -quarkus.kafka-streams.bootstrap-servers=${kafka.bootstrap.servers} -quarkus.kafka-streams.application-id=trace-service -quarkus.kafka-streams.topics=explorviz-spans,token-events -quarkus.kafka-streams.schema-registry-url=http://schema-registry:8081 -%dev.quarkus.kafka-streams.schema-registry-url=http://localhost:8081 - -# Pass through options (directly) to Kafka Streams Config -kafka-streams.commit.interval.ms=5000 -%test.kafka-streams.state.dir=target/data/kafka-data/stores -kafka-streams.default.production.exception.handler=net.explorviz.trace.kafka.IgnoreRecordTooLargeHandler - -# Pass through options (directly) to Kafka Consumer and Producer Configs -#kafka-streams.consumer.fetch.max.wait.ms=500 -kafka-streams.producer.max.request.size=10485760 -kafka-streams.consumer.max.partition.fetch.bytes=10485760 -kafka-streams.consumer.fetch.max.bytes=57671680 - -mp.messaging.outgoing.explorviz-spans.connector=smallrye-kafka -mp.messaging.outgoing.explorviz-spans.topic=explorviz-spans -mp.messaging.outgoing.explorviz-spans.value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer - -mp.messaging.connector.smallrye-kafka.schema.registry.url=http://schema-registry:8081 -%dev.mp.messaging.connector.smallrye-kafka.schema.registry.url=http://localhost:8081 - -# Custom options -explorviz.kafka-streams.topics.in=otlp_spans -explorviz.span.api.timeverification.enabled=${EXPLORVIZ_SPAN_API_TIME_RANGES_ENABLED:true} -explorviz.kafka-streams.topics.in.tokens=token-events -explorviz.kafka-streams.topics.out.spans=explorviz-spans -explorviz.kafka-streams.topics.out.tokens-table=token-events-table - - -explorviz.validate.token-existence=true -%dev.explorviz.validate.token-existence=true -%test.explorviz.validate.token-existence=false - -# discard all incoming records for performance testing -############################# -## ExplorViz Logging Level ## -############################# -quarkus.log.level=INFO -quarkus.log.min-level=TRACE -quarkus.log.category."net.explorviz".level=${EXPLORVIZ_LOG_LVL:INFO} -%dev.quarkus.log.category."net.explorviz".level=${EXPLORVIZ_LOG_LVL:DEBUG} -%test.quarkus.log.category."net.explorviz".level=${EXPLORVIZ_LOG_LVL:TRACE} - -explorviz.log.span.interval=${EXPLORVIZ_LOG_SPAN_INTERVAL:1s} - -##################### -## Jib Image Build ## -##################### -quarkus.container-image.group=explorviz -quarkus.container-image.name=trace-service -quarkus.container-image.tag=latest -quarkus.jib.base-jvm-image=eclipse-temurin:21.0.1_12-jdk-alpine@sha256:075f8207cbfecf7e509e2d93e2ffc36c097b52a8109a9bfd40326fa12bd5c438 diff --git a/old/src/main/resources/explorviz-banner.txt b/old/src/main/resources/explorviz-banner.txt deleted file mode 100644 index 2724f1a..0000000 --- a/old/src/main/resources/explorviz-banner.txt +++ /dev/null @@ -1,11 +0,0 @@ - - - ______ _ __ ___ - | ____| | | \ \ / (_) - | |__ __ ___ __ | | ___ _ _\ \ / / _ ____ - | __| \ \/ / '_ \| |/ _ \| '__\ \/ / | |_ / - | |____ > <| |_) | | (_) | | \ / | |/ / - |______/_/\_\ .__/|_|\___/|_| \/ |_/___| - | | - |_| - diff --git a/old/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt b/old/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt deleted file mode 100644 index 18902cd..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/conversion/TopologyTest.kt +++ /dev/null @@ -1,350 +0,0 @@ -package net.explorviz.trace.adapter.conversion - -import com.google.common.io.BaseEncoding -import com.google.protobuf.ByteString -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.common.v1.KeyValue -import io.opentelemetry.proto.trace.v1.ResourceSpans -import io.opentelemetry.proto.trace.v1.ScopeSpans -import io.opentelemetry.proto.trace.v1.Span -import io.quarkus.test.junit.QuarkusTest -import jakarta.inject.Inject -import java.nio.charset.Charset -import java.util.* -import net.explorviz.trace.adapter.service.TokenService -import net.explorviz.trace.adapter.service.converter.AttributesReader -import net.explorviz.trace.adapter.service.converter.IdHelper -import net.explorviz.avro.EventType -import net.explorviz.avro.LandscapeToken -import net.explorviz.avro.TokenEvent -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.streams.StreamsConfig -import org.apache.kafka.streams.TestInputTopic -import org.apache.kafka.streams.TestOutputTopic -import org.apache.kafka.streams.Topology -import org.apache.kafka.streams.TopologyTestDriver -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -@QuarkusTest -class TopologyTest { - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in") lateinit var inTopic: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.spans") lateinit var spanOutTopicKey: String - - @ConfigProperty(name = "explorviz.kafka-streams.topics.in.tokens") lateinit var tokensInTopic: String - - @Inject lateinit var topology: Topology - - @Inject lateinit var spanSerDe: SpecificAvroSerde - - @Inject lateinit var tokenEventSerDe: SpecificAvroSerde - - private var driver: TopologyTestDriver? = null - private var inputTopic: TestInputTopic? = null - private var inputTopicTokenEvents: TestInputTopic? = null - private var spanOutputTopic: TestOutputTopic? = null - private var tokenEventStore: ReadOnlyKeyValueStore? = null - - @BeforeEach - fun setUp() { - val config = - Properties().apply { - put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String()::class.java.name) - put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, SpecificAvroSerde::class.java.name) - put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, "http://registry:1234") - } - - driver = TopologyTestDriver(topology, config) - - inputTopic = - driver!!.createInputTopic(inTopic, Serdes.ByteArray().serializer(), Serdes.ByteArray().serializer()) - inputTopicTokenEvents = - driver!!.createInputTopic(tokensInTopic, Serdes.String().serializer(), tokenEventSerDe.serializer()) - spanOutputTopic = - driver!!.createOutputTopic(spanOutTopicKey, Serdes.String().deserializer(), spanSerDe.deserializer()) - - tokenEventStore = driver!!.getKeyValueStore("token-events-global-store") - } - - @AfterEach - fun tearDown() { - spanSerDe.close() - driver?.close() - } - - private fun sampleSpan(): Span { - val attributes = - listOf( - KeyValue.newBuilder() - .setKey(AttributesReader.LANDSCAPE_TOKEN) - .setValue(AnyValue.newBuilder().setStringValue("token").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.GIT_COMMIT_CHECKSUM) - .setValue(AnyValue.newBuilder().setStringValue("testGitCommit").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.TOKEN_SECRET) - .setValue(AnyValue.newBuilder().setStringValue("secret").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.HOST_NAME) - .setValue(AnyValue.newBuilder().setStringValue("hostname").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.HOST_IP) - .setValue(AnyValue.newBuilder().setStringValue("1.2.3.4").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.APPLICATION_NAME) - .setValue(AnyValue.newBuilder().setStringValue("appname").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.APPLICATION_INSTANCE_ID) - .setValue(AnyValue.newBuilder().setStringValue("1234").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.APPLICATION_LANGUAGE) - .setValue(AnyValue.newBuilder().setStringValue("language").build()) - .build(), - KeyValue.newBuilder() - .setKey(AttributesReader.METHOD_FQN) - .setValue(AnyValue.newBuilder().setStringValue("net.example.Bar.foo()").build()) - .build(), - ) - - return Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(1668069002431000000L) - .setEndTimeUnixNano(1668072086000000000L) - .addAllAttributes(attributes) - .build() - } - - private fun generateContainerForSpan(span: Span): ExportTraceServiceRequest { - val container2 = ScopeSpans.newBuilder().addSpans(span).build() - val container1 = ResourceSpans.newBuilder().addScopeSpans(container2).build() - return ExportTraceServiceRequest.newBuilder().addResourceSpans(container1).build() - } - - // Tests cause errors since conversion of spans now also triggers persistence of spans - -// @Test -// fun testAttributeTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// val result = spanOutputTopic!!.readKeyValue().value -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// assertEquals(attrs[AttributesReader.LANDSCAPE_TOKEN], result.landscapeToken, "Invalid token") -// assertEquals( -// attrs[AttributesReader.GIT_COMMIT_CHECKSUM], -// result.gitCommitChecksum, -// "Invalid Git Commit Checksum", -// ) -// assertEquals(attrs[AttributesReader.HOST_IP], result.hostIpAddress, "Invalid host ip address") -// assertEquals(attrs[AttributesReader.HOST_NAME], result.hostname, "Invalid host name") -// assertEquals(attrs[AttributesReader.APPLICATION_NAME], result.appName, "Invalid application name") -// assertEquals(attrs[AttributesReader.APPLICATION_INSTANCE_ID], result.appInstanceId, "Invalid application pid") -// assertEquals(attrs[AttributesReader.APPLICATION_LANGUAGE], result.appLanguage, "Invalid application language") -// assertEquals(attrs[AttributesReader.METHOD_FQN], result.fullyQualifiedOperationName, "Invalid operation name") -// } -// -// @Test -// fun testIdTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// -// assertFalse(spanOutputTopic!!.isEmpty, "Output topic is empty, but should contain a data record") -// -// val result = spanOutputTopic!!.readValue() -// -// val sid = BaseEncoding.base16().encode(testSpan.spanId.toByteArray(), 0, 8) -// assertEquals(sid, result.spanId) -// } -// -// @Test -// fun testTimestampTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// val result = spanOutputTopic!!.readKeyValue().value -// -// val expectedTimestamp = sampleSpan().startTimeUnixNano / 1_000_000L -// assertEquals(expectedTimestamp, result.startTimeEpochNano) -// } -// -// @Test -// fun testDynamicTranslation() { -// val testSpan = sampleSpan() -// val containeredSpan = generateContainerForSpan(testSpan) -// -// inputTopic!!.pipeInput(testSpan.spanId.toByteArray(), containeredSpan.toByteArray()) -// val result = spanOutputTopic!!.readValue() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedToken = attrs[AttributesReader.LANDSCAPE_TOKEN] -// val expectedSpanId = IdHelper.convertSpanId(testSpan.spanId.toByteArray()) -// val expectedParentSpanId = IdHelper.convertSpanId(testSpan.parentSpanId.toByteArray()) -// val expectedStartTimeInMillisec = testSpan.startTimeUnixNano / 1_000_000L -// val expectedEndTimeInMillisec = testSpan.endTimeUnixNano / 1_000_000L -// -// assertEquals(expectedToken, result.landscapeToken, "Invalid token") -// assertEquals(expectedSpanId, result.spanId, "Invalid span ID") -// assertEquals(expectedParentSpanId, result.parentSpanId, "Invalid parent span ID") -// assertEquals(expectedStartTimeInMillisec, result.startTimeEpochNano, "Invalid start time") -// assertEquals(expectedEndTimeInMillisec, result.endTimeEpochNano, "Invalid end time") -// } -// -// @Test -// fun testTokenEventCreateInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(EventType.CREATED).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// -// val resultFromStateStore = tokenEventStore!!.get(expectedTokenValue) -// assertEquals(resultFromStateStore, expectedTokenEvent, "Invalid token event in state store") -// } -// -// @Test -// fun testTokenEventDeleteInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(EventType.CREATED).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// -// val tokenService = TokenService(tokenEventStore!!) -// -// val resultFromStateStore = tokenService.validLandscapeTokenValue(expectedTokenValue) -// assertTrue(resultFromStateStore, "Invalid token event in state store") -// -// // Now delete the event -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, null) -// -// val resultFromStateStore2 = tokenService.validLandscapeTokenValue(expectedTokenValue) -// assertFalse(resultFromStateStore2, "Invalid token event in state store, should be null") -// } -// -// @Test -// fun testTokenEventInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(EventType.CREATED).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// -// val tokenService = TokenService(tokenEventStore!!) -// -// val resultFromStateStore = tokenService.validLandscapeTokenValueAndSecret(expectedTokenValue, expectedSecret) -// assertTrue(resultFromStateStore, "Invalid token event in state store") -// -// // Now delete the event -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, null) -// -// val resultFromStateStore2 = tokenService.validLandscapeTokenValueAndSecret(expectedTokenValue, expectedSecret) -// assertFalse(resultFromStateStore2, "Invalid token event in state store, should be null") -// } -// -// @Test -// fun testFilteringTokenEventInteractiveStateStoreQuery() { -// val testSpan = sampleSpan() -// -// val attrs = testSpan.attributesList.associate { it.key to it.value.stringValue } -// -// val expectedTokenValue = attrs[AttributesReader.LANDSCAPE_TOKEN]!! -// val expectedSecret = attrs[AttributesReader.TOKEN_SECRET]!! -// -// val expectedToken = -// LandscapeToken.newBuilder() -// .setSecret(expectedSecret) -// .setValue(expectedTokenValue) -// .setOwnerId("testOwner") -// .setCreated(123L) -// .setAlias("") -// .build() -// -// EventType.values().forEach { eventType -> -// if (eventType != EventType.CREATED) { -// val expectedTokenEvent = -// TokenEvent.newBuilder().setType(eventType).setToken(expectedToken).setClonedToken("").build() -// -// inputTopicTokenEvents!!.pipeInput(expectedTokenValue, expectedTokenEvent) -// } -// } -// -// assertEquals(0, tokenEventStore!!.approximateNumEntries(), "State store not empty, but should be empty") -// -// val tokenService = TokenService(tokenEventStore!!) -// -// val resultFromStateStore = tokenService.validLandscapeTokenValueAndSecret(expectedTokenValue, expectedSecret) -// assertFalse(resultFromStateStore, "Invalid token event in state store") -// } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt b/old/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt deleted file mode 100644 index 7c6bd40..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/injection/MockSchemaRegistryClientProducer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.confluent.kafka.schemaregistry.client.MockSchemaRegistryClient -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient -import io.quarkus.arc.profile.IfBuildProfile -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces - -@Dependent -class MockSchemaRegistryClientProducer { - - @Produces - @IfBuildProfile("test") - fun produceMockSchemaRegistry(): SchemaRegistryClient { - return MockSchemaRegistryClient() - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt b/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt deleted file mode 100644 index 55b426c..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeStructureProducer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.confluent.kafka.schemaregistry.avro.AvroSchema -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.quarkus.arc.profile.IfBuildProfile -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import java.io.IOException -import org.apache.avro.Schema -import org.eclipse.microprofile.config.inject.ConfigProperty - -/** Returns an injectable [SpecificAvroSerde]. */ -@Dependent -class MockSerdeStructureProducer -@Inject -constructor( - var registry: SchemaRegistryClient?, - @ConfigProperty(name = "explorviz.kafka-streams.topics.out.spans") var outTopicSpans: String? = null -) { - - @Produces - @IfBuildProfile("test") - @Throws(IOException::class, RestClientException::class) - fun produceMockSpecificAvroSerde(): SpecificAvroSerde { - registry?.register( - "$outTopicSpans-value", - AvroSchema(Schema.Parser().parse(net.explorviz.avro.Span.`SCHEMA$`.toString())) - ) - - val valueSerde = SpecificAvroSerde(registry) - valueSerde.configure( - mapOf(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG to "http://registry:1234"), - false - ) - return valueSerde - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt b/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt deleted file mode 100644 index 0bf0964..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/injection/MockSerdeTokenEventProducer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.explorviz.trace.adapter.injection - -import io.confluent.kafka.schemaregistry.avro.AvroSchema -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.quarkus.arc.profile.IfBuildProfile -import jakarta.enterprise.context.Dependent -import jakarta.enterprise.inject.Produces -import jakarta.inject.Inject -import java.io.IOException -import net.explorviz.avro.TokenEvent -import org.apache.avro.Schema -import org.eclipse.microprofile.config.inject.ConfigProperty - -/** Returns an injectable {@link SpecificAvroSerde}. */ -@Dependent -class MockSerdeTokenEventProducer -@Inject -constructor( - var registry: SchemaRegistryClient?, - @ConfigProperty(name = "explorviz.kafka-streams.topics.in.tokens") var inTopicToken: String? = null -) { - @Produces - @IfBuildProfile("test") - @Throws(IOException::class, RestClientException::class) - fun produceMockSpecificAvroSerde(): SpecificAvroSerde? { - return if (inTopicToken != null && registry != null) { - registry?.register( - "${inTopicToken}-value", - AvroSchema(Schema.Parser().parse(net.explorviz.avro.TokenEvent.`SCHEMA$`.toString())) - ) - - val valueSerde = SpecificAvroSerde(registry) - valueSerde.configure( - mapOf(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG to "http://registry:1234"), - false - ) - valueSerde - } else { - null - } - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt b/old/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt deleted file mode 100644 index 08187fd..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/service/converter/AttributesReaderTest.kt +++ /dev/null @@ -1,290 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import com.google.protobuf.ByteString -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.common.v1.KeyValue -import io.opentelemetry.proto.trace.v1.Span -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class AttributesReaderTest { - - companion object { - private const val KEY_LANDSCAPE_TOKEN = AttributesReader.LANDSCAPE_TOKEN - private const val KEY_LANDSCAPE_SECRET = AttributesReader.TOKEN_SECRET - private const val KEY_GIT_COMMIT_CHECKSUM = AttributesReader.GIT_COMMIT_CHECKSUM - private const val KEY_HOST_NAME = AttributesReader.HOST_NAME - private const val KEY_HOST_IP = AttributesReader.HOST_IP - private const val KEY_APPLICATION_NAME = AttributesReader.APPLICATION_NAME - private const val KEY_APPLICATION_INSTANCE_ID = AttributesReader.APPLICATION_INSTANCE_ID - private const val KEY_APPLICATION_LANGUAGE = AttributesReader.APPLICATION_LANGUAGE - private const val KEY_METHOD_FQN = AttributesReader.METHOD_FQN - private const val KEY_K8S_POD_NAME = AttributesReader.K8S_POD_NAME - private const val KEY_K8S_NAMESPACE = AttributesReader.K8S_NAMESPACE_NAME - private const val KEY_K8S_DEPLOYMENT = AttributesReader.K8S_DEPLOYMENT_NAME - private const val KEY_K8S_NODE = AttributesReader.K8S_NODE_NAME - - private const val TOKEN = "tok" - private const val SECRET = "secret" - private const val GIT_COMMIT_CHECKSUM = "gitCommitChecksum" - private const val HOSTNAME = "Host" - private const val HOST_IP = "1.2.3.4" - private const val APP_NAME = "Test App" - private const val APP_INSTANCE_ID = "1234L" - private const val APP_LANG = "java" - private const val FQN = "foo.bar.test()" - private const val NAME = "span name" - private const val K8S_POD_NAME = "my name is pod. james pod." - private const val K8S_NAMESPACE = "name " - private const val K8S_DEPLOYMENT = "deployment name" - private const val K8S_NODE = "node.js" - } - - @Test - fun testValidSpanReadOut() { - val attr = generateValidAttributesMap() - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertEquals(reader.landscapeToken, TOKEN) - assertEquals(reader.secret, SECRET) - assertEquals(reader.gitCommitChecksum, GIT_COMMIT_CHECKSUM) - assertEquals(reader.hostName, HOSTNAME) - assertEquals(reader.hostIpAddress, HOST_IP) - assertEquals(reader.applicationName, APP_NAME) - assertEquals(reader.applicationInstanceId, APP_INSTANCE_ID) - assertEquals(reader.applicationLanguage, APP_LANG) - assertEquals(reader.methodFqn, FQN) - assertExcept(reader, null) - } - - @Test - fun testDefaultTokenReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_LANDSCAPE_TOKEN, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.LANDSCAPE_TOKEN) - assertEquals(reader.landscapeToken, DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN) - } - - @Test - fun testDefaultSecretReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_LANDSCAPE_SECRET, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.TOKEN_SECRET) - assertEquals(reader.secret, DefaultAttributeValues.DEFAULT_LANDSCAPE_SECRET) - } - - @Test - fun testDefaultHostnameReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_HOST_NAME, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.HOST_NAME) - assertEquals(reader.hostName, DefaultAttributeValues.DEFAULT_HOST_NAME) - } - - @Test - fun testDefaultHostIpReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_HOST_IP, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.HOST_IP) - assertEquals(reader.hostIpAddress, DefaultAttributeValues.DEFAULT_HOST_IP) - } - - @Test - fun testDefaultAppNameReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_APPLICATION_NAME, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.APPLICATION_NAME) - assertEquals(reader.applicationName, DefaultAttributeValues.DEFAULT_APP_NAME) - } - - @Test - fun testDefaultAppInstanceIdReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_APPLICATION_INSTANCE_ID, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.APPLICATION_INSTANCE_ID) - assertEquals(reader.applicationInstanceId, DefaultAttributeValues.DEFAULT_APP_INSTANCE_ID) - } - - @Test - fun testDefaultAppLangReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.APPLICATION_LANGUAGE) - assertEquals(reader.applicationLanguage, DefaultAttributeValues.DEFAULT_APP_LANG) - } - - @Test - fun testDefaultFqnReadOut() { - var attr = generateValidAttributesMap() - attr = removeElementAndReturnAttributesMap(KEY_METHOD_FQN, attr) - val span = generateSpanFromAttributesMap(attr) - val reader = AttributesReader(span) - - assertExcept(reader, AttributesReader.METHOD_FQN) - assertEquals(reader.methodFqn, DefaultAttributeValues.DEFAULT_CLASS_FQN + "." + span.getName()) - } - - @Test - fun testFqnFromNameReadOut() { - val outerPackage = "net" - val innerPackage = "explorviz" - val className = "Reader" - val methodName = "someNiceMethod()" - - // Only method name given - var span = Span.newBuilder().setName(methodName).build() - var reader = AttributesReader(span) - assertEquals(DefaultAttributeValues.DEFAULT_CLASS_FQN + "." + methodName, reader.methodFqn) - - // Class and method given - span = Span.newBuilder().setName("$className.$methodName").build() - reader = AttributesReader(span) - assertEquals("${DefaultAttributeValues.DEFAULT_PACKAGE_NAME}.${className}.${methodName}", reader.methodFqn) - - // Class and package given - span = Span.newBuilder().setName("$innerPackage.$className.$methodName").build() - reader = AttributesReader(span) - assertEquals("$innerPackage.$className.$methodName", reader.methodFqn) - - // Class and two packages given - span = Span.newBuilder().setName("$outerPackage.$innerPackage.$className.$methodName").build() - reader = AttributesReader(span) - assertEquals("$outerPackage.$innerPackage.$className.$methodName", reader.methodFqn) - } - - private fun assertExcept(reader: AttributesReader, except: String?) { - if (except != AttributesReader.LANDSCAPE_TOKEN) { - assertEquals(reader.landscapeToken, TOKEN) - } - if (except != AttributesReader.TOKEN_SECRET) { - assertEquals(reader.secret, SECRET) - } - if (except != AttributesReader.HOST_NAME) { - assertEquals(reader.hostName, HOSTNAME) - } - if (except != AttributesReader.HOST_IP) { - assertEquals(reader.hostIpAddress, HOST_IP) - } - if (except != AttributesReader.APPLICATION_NAME) { - assertEquals(reader.applicationName, APP_NAME) - } - if (except != AttributesReader.APPLICATION_INSTANCE_ID) { - assertEquals(reader.applicationInstanceId, APP_INSTANCE_ID) - } - if (except != AttributesReader.APPLICATION_LANGUAGE) { - assertEquals(reader.applicationLanguage, APP_LANG) - } - if (except != AttributesReader.METHOD_FQN) { - assertEquals(reader.methodFqn, FQN) - } - - // Kubernetes-related checks - if (except != AttributesReader.K8S_POD_NAME) { - assertEquals(reader.k8sPodName, K8S_POD_NAME) - } - if (except != AttributesReader.K8S_NAMESPACE_NAME) { - assertEquals(reader.k8sNamespace, K8S_NAMESPACE) - } - if (except != AttributesReader.K8S_DEPLOYMENT_NAME) { - assertEquals(reader.k8sDeploymentName, K8S_DEPLOYMENT) - } - if (except != AttributesReader.K8S_NODE_NAME) { - assertEquals(reader.k8sNodeName, K8S_NODE) - } - } - - private fun generateValidAttributesMap(): MutableList { - return mutableListOf( - KeyValue.newBuilder() - .setKey(KEY_LANDSCAPE_TOKEN) - .setValue(AnyValue.newBuilder().setStringValue(TOKEN).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_LANDSCAPE_SECRET) - .setValue(AnyValue.newBuilder().setStringValue(SECRET).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_GIT_COMMIT_CHECKSUM) - .setValue(AnyValue.newBuilder().setStringValue(GIT_COMMIT_CHECKSUM).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_HOST_NAME) - .setValue(AnyValue.newBuilder().setStringValue(HOSTNAME).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_HOST_IP) - .setValue(AnyValue.newBuilder().setStringValue(HOST_IP).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_APPLICATION_NAME) - .setValue(AnyValue.newBuilder().setStringValue(APP_NAME).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_APPLICATION_INSTANCE_ID) - .setValue(AnyValue.newBuilder().setStringValue(APP_INSTANCE_ID).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_APPLICATION_LANGUAGE) - .setValue(AnyValue.newBuilder().setStringValue(APP_LANG).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_METHOD_FQN) - .setValue(AnyValue.newBuilder().setStringValue(FQN).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_POD_NAME) - .setValue(AnyValue.newBuilder().setStringValue(K8S_POD_NAME).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_NAMESPACE) - .setValue(AnyValue.newBuilder().setStringValue(K8S_NAMESPACE).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_DEPLOYMENT) - .setValue(AnyValue.newBuilder().setStringValue(K8S_DEPLOYMENT).build()) - .build(), - KeyValue.newBuilder() - .setKey(KEY_K8S_NODE) - .setValue(AnyValue.newBuilder().setStringValue(K8S_NODE).build()) - .build() - ) - } - - private fun removeElementAndReturnAttributesMap(key: String, map: MutableList): MutableList { - return map.filter { it.key != key }.toMutableList() - } - - private fun generateSpanFromAttributesMap(attributes: MutableList): Span { - return Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f9b9ae7a6", Charsets.UTF_8)) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charsets.UTF_8)) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charsets.UTF_8)) - .setName(NAME) - .setStartTimeUnixNano(1667986986000L) - .setEndTimeUnixNano(1667987046000L) - .addAllAttributes(attributes) - .build() - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt b/old/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt deleted file mode 100644 index 0822e87..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/service/converter/DefaultAttributeValueTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.explorviz.trace.adapter.service.converter - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class DefaultAttributeValueTest { - - @Test - fun testDefaultTokenValue() { - assertEquals(DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN, "mytokenvalue") - } - - @Test - fun testDefaultTokenSecret() { - assertEquals(DefaultAttributeValues.DEFAULT_LANDSCAPE_SECRET, "mytokensecret") - } - - @Test - fun testDefaultHostIp() { - assertEquals(DefaultAttributeValues.DEFAULT_HOST_IP, "0.0.0.0") - } - - @Test - fun testDefaultHostName() { - assertEquals(DefaultAttributeValues.DEFAULT_HOST_NAME, "UNKNOWN-HOST") - } - - @Test - fun testDefaultAppName() { - assertEquals(DefaultAttributeValues.DEFAULT_APP_NAME, "UNKNOWN-APPLICATION") - } - - @Test - fun testDefaultInstanceId() { - assertEquals(DefaultAttributeValues.DEFAULT_APP_INSTANCE_ID, "0") - } - - @Test - fun testDefaultAppLang() { - assertEquals(DefaultAttributeValues.DEFAULT_APP_LANG, "UNKNOWN") - } - - @Test - fun testDefaultFqn() { - // This must adhere to the format , i.e., include at least two '.' - assertEquals(DefaultAttributeValues.DEFAULT_FQN, "unknownpkg.UnknownClass.unknownMethod") - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java b/old/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java deleted file mode 100644 index 3d62458..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/service/converter/SpanConverterImplTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package net.explorviz.trace.adapter.service.converter; - -import com.google.protobuf.ByteString; -import io.opentelemetry.proto.common.v1.AnyValue; -import io.opentelemetry.proto.common.v1.KeyValue; -import io.opentelemetry.proto.trace.v1.Span; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import java.util.HexFormat; -import net.explorviz.trace.persistence.PersistenceSpan; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class SpanConverterImplTest { - - @Inject - SpanConverterImpl spanConverter; - - private Span sampleSpan() { - return Span.newBuilder() - .setTraceId( - ByteString.copyFrom(HexFormat.of().parseHex("50c246ad9c9883d1558df9f19b9ae7a6"))) - .setSpanId(ByteString.copyFrom(HexFormat.of().parseHex("7ef83c66eabd5fbb"))) - .setParentSpanId(ByteString.copyFrom(HexFormat.of().parseHex("7ef83c66efe42aaa"))) - .addAttributes(stringAttr("explorviz.token.id", DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN)) - .addAttributes(stringAttr("git_commit_checksum", "gitchecksum")) - .addAttributes(stringAttr("host_address", "1.2.3.4")) - .addAttributes(stringAttr("host", "testhostname")) - .addAttributes(stringAttr("service.name", "testappname")) - .addAttributes(stringAttr("service.instance.id", "42")) - .addAttributes(stringAttr("telemetry.sdk.language", "java")) - .addAttributes(stringAttr("code.function.name", "net.explorviz.test.Class.doSomething")) - .setStartTimeUnixNano(1668069002431000000L) - .setEndTimeUnixNano(1668072086000000000L) - .build(); - } - - private KeyValue stringAttr(String key, String value) { - return KeyValue.newBuilder() - .setKey(key) - .setValue( - AnyValue.newBuilder() - .setStringValue(value) - .build() - ) - .build(); - } - - private PersistenceSpan resultSpan() { - return new PersistenceSpan(DefaultAttributeValues.DEFAULT_LANDSCAPE_TOKEN, - "gitchecksum", - "7ef83c66eabd5fbb", - "7ef83c66efe42aaa", - "50c246ad9c9883d1558df9f19b9ae7a6", - 1668069002431000000L, - 1668072086000000000L, - "1.2.3.4", - "testhostname", - "testappname", - "java", - "42", - "net/explorviz/test/Class.java", - "doSomething", - "Class", - "", - "", - "", - ""); - } - - @Test - public void testSpanToPersistenceSpanConversion() { - final Span testSpan = this.sampleSpan(); - final PersistenceSpan expectedSpan = this.resultSpan(); - - PersistenceSpan resultSpan = spanConverter.fromOpenTelemetrySpan(testSpan); - - Assertions.assertEquals(expectedSpan, resultSpan); - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt b/old/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt deleted file mode 100644 index 80eebd1..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/service/validation/DefaultSpanValidatorTest.kt +++ /dev/null @@ -1,261 +0,0 @@ -package net.explorviz.trace.adapter.service.validation - -import com.google.protobuf.ByteString -import io.opentelemetry.proto.common.v1.AnyValue -import io.opentelemetry.proto.common.v1.KeyValue -import io.opentelemetry.proto.trace.v1.Span -import java.nio.charset.Charset -import net.explorviz.trace.adapter.service.TokenService -import net.explorviz.trace.adapter.service.converter.AttributesReader -import net.explorviz.trace.adapter.span.service.validation.DefaultSpanValidator -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.Mockito - -class DefaultSpanValidatorTest { - - private companion object { - const val KEY_LANDSCAPE_TOKEN = AttributesReader.LANDSCAPE_TOKEN - const val KEY_LANDSCAPE_SECRET = AttributesReader.TOKEN_SECRET - const val KEY_HOST_NAME = AttributesReader.HOST_NAME - const val KEY_HOST_IP = AttributesReader.HOST_IP - const val KEY_APPLICATION_NAME = AttributesReader.APPLICATION_NAME - const val KEY_APPLICATION_INSTANCE_ID = AttributesReader.APPLICATION_INSTANCE_ID - const val KEY_APPLICATION_LANGUAGE = AttributesReader.APPLICATION_LANGUAGE - const val KEY_METHOD_FQN = AttributesReader.METHOD_FQN - - const val TOKEN = "tok" - const val SECRET = "secret" - const val HOSTNAME = "Host" - const val HOST_IP = "1.2.3.4" - const val APP_NAME = "Test App" - const val APP_INSTANCE_ID = "1234L" - const val APP_LANG = "java" - const val FQN = "foo.bar.test()" - - fun newKeyValueString(key: String, value: String): KeyValue { - return KeyValue.newBuilder() - .setKey(key) - .setValue(AnyValue.newBuilder().setStringValue(value).build()) - .build() - } - } - - private lateinit var validator: DefaultSpanValidator - private lateinit var validSpan: AttributesReader - - @BeforeEach - fun setUp() { - val mockTokenService = Mockito.mock(TokenService::class.java) - Mockito.`when`(mockTokenService.validLandscapeTokenValueAndSecret(Mockito.anyString(), Mockito.anyString())) - .thenReturn(true) - - validator = DefaultSpanValidator(mockTokenService, true) - - validSpan = Mockito.mock(AttributesReader::class.java) - Mockito.`when`(validSpan.secret).thenReturn(SECRET) - Mockito.`when`(validSpan.landscapeToken).thenReturn(TOKEN) - Mockito.`when`(validSpan.hostName).thenReturn(HOSTNAME) - Mockito.`when`(validSpan.hostIpAddress).thenReturn(HOST_IP) - Mockito.`when`(validSpan.applicationName).thenReturn(APP_NAME) - Mockito.`when`(validSpan.applicationInstanceId).thenReturn(APP_INSTANCE_ID) - Mockito.`when`(validSpan.applicationLanguage).thenReturn(APP_LANG) - Mockito.`when`(validSpan.methodFqn).thenReturn(FQN) - } - - private fun generateValidAttributesMap(): List { - return listOf( - newKeyValueString(KEY_LANDSCAPE_TOKEN, TOKEN), - newKeyValueString(KEY_LANDSCAPE_SECRET, SECRET), - newKeyValueString(KEY_HOST_NAME, HOSTNAME), - newKeyValueString(KEY_HOST_IP, HOST_IP), - newKeyValueString(KEY_APPLICATION_NAME, APP_NAME), - newKeyValueString(KEY_APPLICATION_INSTANCE_ID, APP_INSTANCE_ID), - newKeyValueString(KEY_APPLICATION_LANGUAGE, APP_LANG), - newKeyValueString(KEY_METHOD_FQN, FQN), - ) - } - - private fun generateSpanFromAttributesMap(attributes: List): Span { - return Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(1667986986000L) - .setEndTimeUnixNano(1667987046000L) - .addAllAttributes(attributes) - .build() - } - - private fun removeElementAndReturnAttributesMap( - keyToBeRemoved: String, - attributes: List - ): List { - return attributes.filterNot { it.key == keyToBeRemoved } - } - - private fun replaceElementAndReturnAttributesMap( - key: String, - newVal: String, - attributes: List - ): List { - return attributes.map { if (it.key == key) newKeyValueString(key, newVal) else it } - } - - @Test - fun valid() { - val attrMap = generateValidAttributesMap() - val valid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(valid)) - } - - @Test - fun invalidLandscapeTokenValue() { - for (invalidTokenValue in listOf("", "\n", "\t", " ")) { - var attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_LANDSCAPE_TOKEN, invalidTokenValue, attrMap) - val invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - - @Test - fun invalidLandscapeTokenSecret() { - for (invalidTokenSecret in listOf("", "\n", "\t", " ")) { - var attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_LANDSCAPE_SECRET, invalidTokenSecret, attrMap) - val invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - - @Test - fun testHost() { - var attrMap = generateValidAttributesMap() - - attrMap = removeElementAndReturnAttributesMap(KEY_HOST_NAME, attrMap) - var invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = removeElementAndReturnAttributesMap(KEY_HOST_IP, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_HOST_IP, " ", attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - - for (invalidHostName in listOf("", "\n", "\t", " ")) { - for (invalidHostIp in listOf("", "\t", "\n", " ")) { - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_HOST_NAME, invalidHostName, attrMap) - attrMap = replaceElementAndReturnAttributesMap(KEY_HOST_IP, invalidHostIp, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - } - - @Test - fun testApp() { - var attrMap = generateValidAttributesMap() - - attrMap = removeElementAndReturnAttributesMap(KEY_APPLICATION_NAME, attrMap) - var invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = removeElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertTrue(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_NAME, " ", attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, " ", attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - - for (invalidId in listOf("", "\n", "\t", " ")) { - for (invalidLanguage in listOf("", "\t", "\n", " ")) { - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_NAME, invalidId, attrMap) - attrMap = replaceElementAndReturnAttributesMap(KEY_APPLICATION_LANGUAGE, invalidLanguage, attrMap) - invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - } - - @Test - fun testOperation() { - var attrMap = generateValidAttributesMap() - - val invalidValues = - listOf( - "", - "\n", - "\t", - " ", - "noMethod", - "...", - "a..", - "a.b.", - "a.b. ", - "a..c", - ".b.c", - "..c", - ".b.", - ) - for (invalidMethodFqn in invalidValues) { - attrMap = generateValidAttributesMap() - attrMap = replaceElementAndReturnAttributesMap(KEY_METHOD_FQN, invalidMethodFqn, attrMap) - val invalid = generateSpanFromAttributesMap(attrMap) - assertFalse(validator.isValid(invalid)) - } - } - - @Test - fun testTimestamps() { - val attrMap = generateValidAttributesMap() - - val invalids = - listOf( - Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(0L) - .setEndTimeUnixNano(456L) - .addAllAttributes(attrMap) - .build(), - Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(456L) - .setEndTimeUnixNano(0L) - .addAllAttributes(attrMap) - .build(), - Span.newBuilder() - .setTraceId(ByteString.copyFrom("50c246ad9c9883d1558df9f19b9ae7a6", Charset.defaultCharset())) - .setSpanId(ByteString.copyFrom("7ef83c66eabd5fbb", Charset.defaultCharset())) - .setParentSpanId(ByteString.copyFrom("7ef83c66efe42aaa", Charset.defaultCharset())) - .setStartTimeUnixNano(0L) - .setEndTimeUnixNano(0L) - .addAllAttributes(attrMap) - .build(), - ) - - for (invalid in invalids) { - assertFalse(validator.isValid(invalid)) - } - } -} diff --git a/old/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt b/old/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt deleted file mode 100644 index 3643789..0000000 --- a/old/src/test/java/net/explorviz/trace/adapter/translation/IdHelperTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.explorviz.trace.adapter.translation - -import com.google.common.io.BaseEncoding -import net.explorviz.trace.adapter.service.converter.IdHelper -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class IdHelperTest { - - @Test - fun spanId() { - val id = "7ef83c66eabd5fbb" - val spanId = BaseEncoding.base16().lowerCase().decode(id) - val convertedSpanId = IdHelper.convertSpanId(spanId) - assertEquals("7ef83c66eabd5fbb", convertedSpanId) - } - - @Test - fun traceId() { - val id = "50c246ad9c9883d1558df9f19b9ae7a6" - val traceId = BaseEncoding.base16().lowerCase().decode(id) - val convertedTraceId = IdHelper.convertTraceId(traceId) - assertEquals("50c246ad9c9883d1558df9f19b9ae7a6", convertedTraceId) - } -} From 41a19f980bb9246cd40274b23a341b1f3b382cd2 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Mon, 15 Jun 2026 10:30:18 +0200 Subject: [PATCH 06/27] Token event handling, file restructure --- cmd/trace-service/main.go | 173 ----------- internal/attrib/attributes.go | 31 ++ .../common.go => attrib/spanreader.go} | 15 +- internal/conversion/protobuf.go | 45 +++ internal/conversion/span.go | 40 +-- ...rsistence_span.pb.go => parsed_span.pb.go} | 123 ++++---- internal/genproto/tokenpb/token_event.pb.go | 294 ++++++++++++++++++ internal/kafka/spanproc/spanproc.go | 124 ++++++++ internal/kafka/tokenproc/tokenproc.go | 73 +++++ internal/{conversion => }/parsing/code.go | 3 +- internal/parsing/common.go | 9 + internal/{conversion => }/parsing/doc.go | 0 internal/{conversion => }/parsing/fqn.go | 0 main.go | 98 ++++++ ...rsistence_span.proto => parsed_span.proto} | 2 +- proto/token_event.proto | 25 ++ scripts/genproto.go | 40 +++ 17 files changed, 820 insertions(+), 275 deletions(-) delete mode 100644 cmd/trace-service/main.go create mode 100644 internal/attrib/attributes.go rename internal/{conversion/parsing/common.go => attrib/spanreader.go} (77%) create mode 100644 internal/conversion/protobuf.go rename internal/genproto/spanpb/{persistence_span.pb.go => parsed_span.pb.go} (69%) create mode 100644 internal/genproto/tokenpb/token_event.pb.go create mode 100644 internal/kafka/spanproc/spanproc.go create mode 100644 internal/kafka/tokenproc/tokenproc.go rename internal/{conversion => }/parsing/code.go (91%) create mode 100644 internal/parsing/common.go rename internal/{conversion => }/parsing/doc.go (100%) rename internal/{conversion => }/parsing/fqn.go (100%) create mode 100644 main.go rename proto/{persistence_span.proto => parsed_span.proto} (96%) create mode 100644 proto/token_event.proto create mode 100644 scripts/genproto.go diff --git a/cmd/trace-service/main.go b/cmd/trace-service/main.go deleted file mode 100644 index 1aa5583..0000000 --- a/cmd/trace-service/main.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log/slog" - "os" - "os/signal" - "runtime" - "sync/atomic" - "time" - - "github.com/ExplorViz/trace-service/internal/conversion" - "github.com/ExplorViz/trace-service/internal/conversion/parsing" - "github.com/ExplorViz/trace-service/internal/genproto/spanpb" - - "github.com/twmb/franz-go/pkg/kgo" - - "google.golang.org/protobuf/proto" - - coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" -) - -var ( - seedBroker = flag.String("broker", "localhost:9092", "network endpoint of the Kafka broker to use (:)") - inTopic = flag.String("topic-in", "test", "Kafka topic to consume OpenTelemetry spans from") - outTopic = flag.String("topic-out", "explorviz.spans.out", "Kafka topic to produce parsed spans into") - logInterval = flag.Duration("log-interval", time.Second, "interval at which received spans should be logged (e.g. \"5s\")") - - lastReceivedSpans atomic.Int64 -) - -func main() { - flag.Parse() - - cl, err := kgo.NewClient( - kgo.SeedBrokers(*seedBroker), - kgo.DefaultProduceTopic(*outTopic), - kgo.ConsumeTopics(*inTopic), - ) - if err != nil { - slog.Error("unable to initialize kgo client", "error", err) - os.Exit(1) - } - defer cl.Close() - - sigs := make(chan os.Signal, 2) - signal.Notify(sigs, os.Interrupt) - go func() { - <-sigs - fmt.Println("received interrupt signal; closing client") - cl.Close() - <-sigs - fmt.Println("received second interrupt; exiting") - os.Exit(1) - }() - - fmt.Print(` - ______ _ __ ___ - | ____| | | \ \ / (_) - | |__ __ ___ __ | | ___ _ _\ \ / / _ ____ - | __| \ \/ / '_ \| |/ _ \| '__\ \/ / | |_ / - | |____ > <| |_) | | (_) | | \ / | |/ / - |______/_/\_\ .__/|_|\___/|_| \/ |_/___| - | | - |_| trace-service - -`) - - go func() { - ticker := time.NewTicker(*logInterval) - defer ticker.Stop() - - for range ticker.C { - count := lastReceivedSpans.Swap(0) - slog.Info(fmt.Sprintf("received %d spans", count)) - } - }() - - spans := make(chan parsing.SpanReader) - results := make(chan *spanpb.SpanData) - - workerCount := runtime.NumCPU() - for range workerCount { - go spanWorker(spans, results) - } - go producerWorker(results, cl) - - for { - fs := cl.PollFetches(context.Background()) - fs.EachRecord(func(r *kgo.Record) { - var req coltracepb.ExportTraceServiceRequest - - if err := proto.Unmarshal(r.Value, &req); err != nil { - slog.Debug("invalid protocol buffer", "error", err) - return - } - - for _, rs := range req.ResourceSpans { - for _, ss := range rs.ScopeSpans { - for _, s := range ss.Spans { - spans <- parsing.NewSpanReader(s, ss.Scope, rs.Resource) - } - } - } - }) - } -} - -func spanWorker(spans <-chan parsing.SpanReader, results chan<- *spanpb.SpanData) { - for s := range spans { - p, err := conversion.ConvertSpan(s) - if err != nil { - slog.Error("failed to convert span", "error", err) - continue - } - results <- toProto(p) - } -} - -func producerWorker(results <-chan *spanpb.SpanData, cl *kgo.Client) { - for s := range results { - out, err := proto.Marshal(s) - if err != nil { - slog.Error("failed to encode protobuf", "error", err) - continue - } - cl.ProduceSync(context.Background(), &kgo.Record{ - Key: []byte(s.LandscapeTokenId), - Value: out, - }) - } -} - -func toProto(ps conversion.PersistenceSpan) *spanpb.SpanData { - s := spanpb.SpanData{ - LandscapeTokenId: ps.LandscapeTokenId, - LandscapeTokenSecret: ps.LandscapeTokenSecret, - - TraceId: ps.TraceId, - SpanId: ps.SpanId, - SpanName: ps.SpanName, - ParentId: strOrNil(ps.ParentSpanId), - - StartTime: ps.StartTime, - EndTime: ps.EndTime, - - ApplicationName: ps.ApplicationName, - } - - switch e := ps.Entity.(type) { - case parsing.CodeSpanEntity: - s.EntityDescriptor = &spanpb.SpanData_CodeDescriptor{ - CodeDescriptor: &spanpb.CodeDescriptor{ - FilePath: e.FilePath, - FunctionName: e.FuncName, - ClassName: strOrNil(e.ClassName), - Language: strOrNil(e.Language), - GitCommitHash: strOrNil(e.GitCommitHash), - }, - } - } - - return &s -} - -func strOrNil(s string) *string { - if s == "" { - return nil - } - return &s -} diff --git a/internal/attrib/attributes.go b/internal/attrib/attributes.go new file mode 100644 index 0000000..882ada6 --- /dev/null +++ b/internal/attrib/attributes.go @@ -0,0 +1,31 @@ +// Package attrib is concerned with OpenTelemetry attributes. +// It defines ExplorViz-specific attributes and provides utilities +// for efficiently retrieving attributes from OTLP data. +package attrib + +import "go.opentelemetry.io/otel/attribute" + +type ExplorVizAttribute struct { + Key attribute.Key + DefaultValue string +} + +// ExplorVizAttributes defines attributes required or written by ExplorViz itself. +var ExplorVizAttributes = struct { + LandscapeTokenID ExplorVizAttribute + LandscapeTokenSecret ExplorVizAttribute + EntityId ExplorVizAttribute +}{ + LandscapeTokenID: ExplorVizAttribute{ + Key: "explorviz.token.id", + DefaultValue: "mytokenvalue", + }, + LandscapeTokenSecret: ExplorVizAttribute{ + Key: "explorviz.token.secret", + DefaultValue: "mytokenvalue", + }, + EntityId: ExplorVizAttribute{ + Key: "explorviz.entity.id", + DefaultValue: "unknown", + }, +} diff --git a/internal/conversion/parsing/common.go b/internal/attrib/spanreader.go similarity index 77% rename from internal/conversion/parsing/common.go rename to internal/attrib/spanreader.go index 7d70def..b5d8f39 100644 --- a/internal/conversion/parsing/common.go +++ b/internal/attrib/spanreader.go @@ -1,4 +1,4 @@ -package parsing +package attrib import ( "go.opentelemetry.io/otel/attribute" @@ -8,6 +8,9 @@ import ( tracepb "go.opentelemetry.io/proto/otlp/trace/v1" ) +// A SpanReader groups a Protobuf [tracepb.Span] together with its [commonpb.InstrumentationScope] +// and [resourcepb.Resource]. It provides helper methods for efficient lookup of attributes +// by leveraging pre-constructed maps. type SpanReader struct { Span *tracepb.Span Scope *commonpb.InstrumentationScope @@ -22,6 +25,10 @@ func (s SpanReader) SpanAttribute(key attribute.Key) *commonpb.AnyValue { return s.spanAttributes[string(key)] } +func (s SpanReader) ScopeAttribute(key attribute.Key) *commonpb.AnyValue { + return s.scopeAttributes[string(key)] +} + func (s SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { return s.resourceAttributes[string(key)] } @@ -34,12 +41,6 @@ func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resou } } -type SpanParser func(s SpanReader) (SpanEntity, error) - -type SpanEntity interface { - Id() string -} - func attrsToMap(attrs []*commonpb.KeyValue) map[string]*commonpb.AnyValue { m := make(map[string]*commonpb.AnyValue, len(attrs)) for _, kv := range attrs { diff --git a/internal/conversion/protobuf.go b/internal/conversion/protobuf.go new file mode 100644 index 0000000..e33756b --- /dev/null +++ b/internal/conversion/protobuf.go @@ -0,0 +1,45 @@ +package conversion + +import ( + "github.com/ExplorViz/trace-service/internal/genproto/spanpb" + "github.com/ExplorViz/trace-service/internal/parsing" +) + +func ToProto(ps ParsedSpan) *spanpb.ParsedSpan { + s := spanpb.ParsedSpan{ + LandscapeTokenId: ps.LandscapeTokenId, + LandscapeTokenSecret: ps.LandscapeTokenSecret, + + TraceId: ps.TraceId, + SpanId: ps.SpanId, + SpanName: ps.SpanName, + ParentId: strOrNil(ps.ParentSpanId), + + StartTime: ps.StartTime, + EndTime: ps.EndTime, + + ApplicationName: ps.ApplicationName, + } + + switch e := ps.Entity.(type) { + case parsing.CodeSpanEntity: + s.EntityDescriptor = &spanpb.ParsedSpan_CodeDescriptor{ + CodeDescriptor: &spanpb.CodeDescriptor{ + FilePath: e.FilePath, + FunctionName: e.FuncName, + ClassName: strOrNil(e.ClassName), + Language: strOrNil(e.Language), + GitCommitHash: strOrNil(e.GitCommitHash), + }, + } + } + + return &s +} + +func strOrNil(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/internal/conversion/span.go b/internal/conversion/span.go index dbcb382..aad0726 100644 --- a/internal/conversion/span.go +++ b/internal/conversion/span.go @@ -4,36 +4,12 @@ import ( "errors" "log/slog" - "github.com/ExplorViz/trace-service/internal/conversion/parsing" - "go.opentelemetry.io/otel/attribute" + "github.com/ExplorViz/trace-service/internal/attrib" + "github.com/ExplorViz/trace-service/internal/parsing" semconv "go.opentelemetry.io/otel/semconv/v1.41.0" ) -type ExplorVizAttribute struct { - Key attribute.Key - DefaultValue string -} - -var ExplorVizAttributes = struct { - LandscapeTokenID ExplorVizAttribute - LandscapeTokenSecret ExplorVizAttribute - EntityId ExplorVizAttribute -}{ - LandscapeTokenID: ExplorVizAttribute{ - Key: "explorviz.token.id", - DefaultValue: "mytokenvalue", - }, - LandscapeTokenSecret: ExplorVizAttribute{ - Key: "explorviz.token.secret", - DefaultValue: "mytokenvalue", - }, - EntityId: ExplorVizAttribute{ - Key: "explorviz.entity.id", - DefaultValue: "unknown", - }, -} - -type PersistenceSpan struct { +type ParsedSpan struct { LandscapeTokenId string LandscapeTokenSecret string @@ -53,9 +29,9 @@ var parserChain = []parsing.SpanParser{ parsing.ParseCodeSpan, } -func ConvertSpan(sr parsing.SpanReader) (PersistenceSpan, error) { - tokenId := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() - tokenSecret := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() +func ConvertSpan(sr *attrib.SpanReader) (ParsedSpan, error) { + tokenId := sr.ResourceAttribute(attrib.ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() + tokenSecret := sr.ResourceAttribute(attrib.ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() appName := sr.ResourceAttribute(semconv.ServiceNameKey).GetStringValue() for _, parser := range parserChain { @@ -64,7 +40,7 @@ func ConvertSpan(sr parsing.SpanReader) (PersistenceSpan, error) { slog.Debug("parser failed", "error", err) continue } - return PersistenceSpan{ + return ParsedSpan{ LandscapeTokenId: tokenId, LandscapeTokenSecret: tokenSecret, @@ -79,5 +55,5 @@ func ConvertSpan(sr parsing.SpanReader) (PersistenceSpan, error) { Entity: res, }, nil } - return PersistenceSpan{}, errors.New("no matching span parser found") + return ParsedSpan{}, errors.New("no matching span parser found") } diff --git a/internal/genproto/spanpb/persistence_span.pb.go b/internal/genproto/spanpb/parsed_span.pb.go similarity index 69% rename from internal/genproto/spanpb/persistence_span.pb.go rename to internal/genproto/spanpb/parsed_span.pb.go index 7d5a164..ddd29b4 100644 --- a/internal/genproto/spanpb/persistence_span.pb.go +++ b/internal/genproto/spanpb/parsed_span.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.11 // protoc v3.19.6 -// source: persistence_span.proto +// source: parsed_span.proto package spanpb @@ -21,7 +21,7 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type SpanData struct { +type ParsedSpan struct { state protoimpl.MessageState `protogen:"open.v1"` LandscapeTokenId string `protobuf:"bytes,1,opt,name=landscape_token_id,json=landscapeTokenId,proto3" json:"landscape_token_id,omitempty"` LandscapeTokenSecret string `protobuf:"bytes,2,opt,name=landscape_token_secret,json=landscapeTokenSecret,proto3" json:"landscape_token_secret,omitempty"` @@ -34,27 +34,27 @@ type SpanData struct { ApplicationName string `protobuf:"bytes,9,opt,name=application_name,json=applicationName,proto3" json:"application_name,omitempty"` // Types that are valid to be assigned to EntityDescriptor: // - // *SpanData_CodeDescriptor - EntityDescriptor isSpanData_EntityDescriptor `protobuf_oneof:"entity_descriptor"` + // *ParsedSpan_CodeDescriptor + EntityDescriptor isParsedSpan_EntityDescriptor `protobuf_oneof:"entity_descriptor"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SpanData) Reset() { - *x = SpanData{} - mi := &file_persistence_span_proto_msgTypes[0] +func (x *ParsedSpan) Reset() { + *x = ParsedSpan{} + mi := &file_parsed_span_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SpanData) String() string { +func (x *ParsedSpan) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SpanData) ProtoMessage() {} +func (*ParsedSpan) ProtoMessage() {} -func (x *SpanData) ProtoReflect() protoreflect.Message { - mi := &file_persistence_span_proto_msgTypes[0] +func (x *ParsedSpan) ProtoReflect() protoreflect.Message { + mi := &file_parsed_span_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -65,99 +65,99 @@ func (x *SpanData) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SpanData.ProtoReflect.Descriptor instead. -func (*SpanData) Descriptor() ([]byte, []int) { - return file_persistence_span_proto_rawDescGZIP(), []int{0} +// Deprecated: Use ParsedSpan.ProtoReflect.Descriptor instead. +func (*ParsedSpan) Descriptor() ([]byte, []int) { + return file_parsed_span_proto_rawDescGZIP(), []int{0} } -func (x *SpanData) GetLandscapeTokenId() string { +func (x *ParsedSpan) GetLandscapeTokenId() string { if x != nil { return x.LandscapeTokenId } return "" } -func (x *SpanData) GetLandscapeTokenSecret() string { +func (x *ParsedSpan) GetLandscapeTokenSecret() string { if x != nil { return x.LandscapeTokenSecret } return "" } -func (x *SpanData) GetTraceId() string { +func (x *ParsedSpan) GetTraceId() string { if x != nil { return x.TraceId } return "" } -func (x *SpanData) GetSpanId() string { +func (x *ParsedSpan) GetSpanId() string { if x != nil { return x.SpanId } return "" } -func (x *SpanData) GetSpanName() string { +func (x *ParsedSpan) GetSpanName() string { if x != nil { return x.SpanName } return "" } -func (x *SpanData) GetParentId() string { +func (x *ParsedSpan) GetParentId() string { if x != nil && x.ParentId != nil { return *x.ParentId } return "" } -func (x *SpanData) GetStartTime() uint64 { +func (x *ParsedSpan) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } -func (x *SpanData) GetEndTime() uint64 { +func (x *ParsedSpan) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } -func (x *SpanData) GetApplicationName() string { +func (x *ParsedSpan) GetApplicationName() string { if x != nil { return x.ApplicationName } return "" } -func (x *SpanData) GetEntityDescriptor() isSpanData_EntityDescriptor { +func (x *ParsedSpan) GetEntityDescriptor() isParsedSpan_EntityDescriptor { if x != nil { return x.EntityDescriptor } return nil } -func (x *SpanData) GetCodeDescriptor() *CodeDescriptor { +func (x *ParsedSpan) GetCodeDescriptor() *CodeDescriptor { if x != nil { - if x, ok := x.EntityDescriptor.(*SpanData_CodeDescriptor); ok { + if x, ok := x.EntityDescriptor.(*ParsedSpan_CodeDescriptor); ok { return x.CodeDescriptor } } return nil } -type isSpanData_EntityDescriptor interface { - isSpanData_EntityDescriptor() +type isParsedSpan_EntityDescriptor interface { + isParsedSpan_EntityDescriptor() } -type SpanData_CodeDescriptor struct { +type ParsedSpan_CodeDescriptor struct { CodeDescriptor *CodeDescriptor `protobuf:"bytes,10,opt,name=code_descriptor,json=codeDescriptor,proto3,oneof"` } -func (*SpanData_CodeDescriptor) isSpanData_EntityDescriptor() {} +func (*ParsedSpan_CodeDescriptor) isParsedSpan_EntityDescriptor() {} type CodeDescriptor struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -173,7 +173,7 @@ type CodeDescriptor struct { func (x *CodeDescriptor) Reset() { *x = CodeDescriptor{} - mi := &file_persistence_span_proto_msgTypes[1] + mi := &file_parsed_span_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -185,7 +185,7 @@ func (x *CodeDescriptor) String() string { func (*CodeDescriptor) ProtoMessage() {} func (x *CodeDescriptor) ProtoReflect() protoreflect.Message { - mi := &file_persistence_span_proto_msgTypes[1] + mi := &file_parsed_span_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -198,7 +198,7 @@ func (x *CodeDescriptor) ProtoReflect() protoreflect.Message { // Deprecated: Use CodeDescriptor.ProtoReflect.Descriptor instead. func (*CodeDescriptor) Descriptor() ([]byte, []int) { - return file_persistence_span_proto_rawDescGZIP(), []int{1} + return file_parsed_span_proto_rawDescGZIP(), []int{1} } func (x *CodeDescriptor) GetId() string { @@ -243,12 +243,13 @@ func (x *CodeDescriptor) GetGitCommitHash() string { return "" } -var File_persistence_span_proto protoreflect.FileDescriptor +var File_parsed_span_proto protoreflect.FileDescriptor -const file_persistence_span_proto_rawDesc = "" + +const file_parsed_span_proto_rawDesc = "" + "\n" + - "\x16persistence_span.proto\"\xa5\x03\n" + - "\bSpanData\x12,\n" + + "\x11parsed_span.proto\"\xa7\x03\n" + + "\n" + + "ParsedSpan\x12,\n" + "\x12landscape_token_id\x18\x01 \x01(\tR\x10landscapeTokenId\x124\n" + "\x16landscape_token_secret\x18\x02 \x01(\tR\x14landscapeTokenSecret\x12\x19\n" + "\btrace_id\x18\x03 \x01(\tR\atraceId\x12\x17\n" + @@ -277,24 +278,24 @@ const file_persistence_span_proto_rawDesc = "" + "\x10_git_commit_hashB=Z;github.com/ExplorViz/trace-service/internal/genproto/spanpbb\x06proto3" var ( - file_persistence_span_proto_rawDescOnce sync.Once - file_persistence_span_proto_rawDescData []byte + file_parsed_span_proto_rawDescOnce sync.Once + file_parsed_span_proto_rawDescData []byte ) -func file_persistence_span_proto_rawDescGZIP() []byte { - file_persistence_span_proto_rawDescOnce.Do(func() { - file_persistence_span_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_persistence_span_proto_rawDesc), len(file_persistence_span_proto_rawDesc))) +func file_parsed_span_proto_rawDescGZIP() []byte { + file_parsed_span_proto_rawDescOnce.Do(func() { + file_parsed_span_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_parsed_span_proto_rawDesc), len(file_parsed_span_proto_rawDesc))) }) - return file_persistence_span_proto_rawDescData + return file_parsed_span_proto_rawDescData } -var file_persistence_span_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_persistence_span_proto_goTypes = []any{ - (*SpanData)(nil), // 0: SpanData +var file_parsed_span_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_parsed_span_proto_goTypes = []any{ + (*ParsedSpan)(nil), // 0: ParsedSpan (*CodeDescriptor)(nil), // 1: CodeDescriptor } -var file_persistence_span_proto_depIdxs = []int32{ - 1, // 0: SpanData.code_descriptor:type_name -> CodeDescriptor +var file_parsed_span_proto_depIdxs = []int32{ + 1, // 0: ParsedSpan.code_descriptor:type_name -> CodeDescriptor 1, // [1:1] is the sub-list for method output_type 1, // [1:1] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name @@ -302,30 +303,30 @@ var file_persistence_span_proto_depIdxs = []int32{ 0, // [0:1] is the sub-list for field type_name } -func init() { file_persistence_span_proto_init() } -func file_persistence_span_proto_init() { - if File_persistence_span_proto != nil { +func init() { file_parsed_span_proto_init() } +func file_parsed_span_proto_init() { + if File_parsed_span_proto != nil { return } - file_persistence_span_proto_msgTypes[0].OneofWrappers = []any{ - (*SpanData_CodeDescriptor)(nil), + file_parsed_span_proto_msgTypes[0].OneofWrappers = []any{ + (*ParsedSpan_CodeDescriptor)(nil), } - file_persistence_span_proto_msgTypes[1].OneofWrappers = []any{} + file_parsed_span_proto_msgTypes[1].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_persistence_span_proto_rawDesc), len(file_persistence_span_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_parsed_span_proto_rawDesc), len(file_parsed_span_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_persistence_span_proto_goTypes, - DependencyIndexes: file_persistence_span_proto_depIdxs, - MessageInfos: file_persistence_span_proto_msgTypes, + GoTypes: file_parsed_span_proto_goTypes, + DependencyIndexes: file_parsed_span_proto_depIdxs, + MessageInfos: file_parsed_span_proto_msgTypes, }.Build() - File_persistence_span_proto = out.File - file_persistence_span_proto_goTypes = nil - file_persistence_span_proto_depIdxs = nil + File_parsed_span_proto = out.File + file_parsed_span_proto_goTypes = nil + file_parsed_span_proto_depIdxs = nil } diff --git a/internal/genproto/tokenpb/token_event.pb.go b/internal/genproto/tokenpb/token_event.pb.go new file mode 100644 index 0000000..8eef0fa --- /dev/null +++ b/internal/genproto/tokenpb/token_event.pb.go @@ -0,0 +1,294 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.19.6 +// source: token_event.proto + +package tokenpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EventType int32 + +const ( + EventType_CREATED EventType = 0 + EventType_DELETED EventType = 1 + EventType_CLONED EventType = 2 + EventType_ACCESS_GRANTED EventType = 3 + EventType_ACCESS_REVOKED EventType = 4 +) + +// Enum value maps for EventType. +var ( + EventType_name = map[int32]string{ + 0: "CREATED", + 1: "DELETED", + 2: "CLONED", + 3: "ACCESS_GRANTED", + 4: "ACCESS_REVOKED", + } + EventType_value = map[string]int32{ + "CREATED": 0, + "DELETED": 1, + "CLONED": 2, + "ACCESS_GRANTED": 3, + "ACCESS_REVOKED": 4, + } +) + +func (x EventType) Enum() *EventType { + p := new(EventType) + *p = x + return p +} + +func (x EventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EventType) Descriptor() protoreflect.EnumDescriptor { + return file_token_event_proto_enumTypes[0].Descriptor() +} + +func (EventType) Type() protoreflect.EnumType { + return &file_token_event_proto_enumTypes[0] +} + +func (x EventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EventType.Descriptor instead. +func (EventType) EnumDescriptor() ([]byte, []int) { + return file_token_event_proto_rawDescGZIP(), []int{0} +} + +type TokenEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type EventType `protobuf:"varint,1,opt,name=type,proto3,enum=EventType" json:"type,omitempty"` + Token *LandscapeToken `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + ClonedTokenId *string `protobuf:"bytes,3,opt,name=cloned_token_id,json=clonedTokenId,proto3,oneof" json:"cloned_token_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TokenEvent) Reset() { + *x = TokenEvent{} + mi := &file_token_event_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TokenEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenEvent) ProtoMessage() {} + +func (x *TokenEvent) ProtoReflect() protoreflect.Message { + mi := &file_token_event_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenEvent.ProtoReflect.Descriptor instead. +func (*TokenEvent) Descriptor() ([]byte, []int) { + return file_token_event_proto_rawDescGZIP(), []int{0} +} + +func (x *TokenEvent) GetType() EventType { + if x != nil { + return x.Type + } + return EventType_CREATED +} + +func (x *TokenEvent) GetToken() *LandscapeToken { + if x != nil { + return x.Token + } + return nil +} + +func (x *TokenEvent) GetClonedTokenId() string { + if x != nil && x.ClonedTokenId != nil { + return *x.ClonedTokenId + } + return "" +} + +type LandscapeToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + Alias string `protobuf:"bytes,3,opt,name=alias,proto3" json:"alias,omitempty"` + OwnerId string `protobuf:"bytes,4,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + Created uint64 `protobuf:"varint,5,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LandscapeToken) Reset() { + *x = LandscapeToken{} + mi := &file_token_event_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LandscapeToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LandscapeToken) ProtoMessage() {} + +func (x *LandscapeToken) ProtoReflect() protoreflect.Message { + mi := &file_token_event_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LandscapeToken.ProtoReflect.Descriptor instead. +func (*LandscapeToken) Descriptor() ([]byte, []int) { + return file_token_event_proto_rawDescGZIP(), []int{1} +} + +func (x *LandscapeToken) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *LandscapeToken) GetSecret() string { + if x != nil { + return x.Secret + } + return "" +} + +func (x *LandscapeToken) GetAlias() string { + if x != nil { + return x.Alias + } + return "" +} + +func (x *LandscapeToken) GetOwnerId() string { + if x != nil { + return x.OwnerId + } + return "" +} + +func (x *LandscapeToken) GetCreated() uint64 { + if x != nil { + return x.Created + } + return 0 +} + +var File_token_event_proto protoreflect.FileDescriptor + +const file_token_event_proto_rawDesc = "" + + "\n" + + "\x11token_event.proto\"\x94\x01\n" + + "\n" + + "TokenEvent\x12\x1e\n" + + "\x04type\x18\x01 \x01(\x0e2\n" + + ".EventTypeR\x04type\x12%\n" + + "\x05token\x18\x02 \x01(\v2\x0f.LandscapeTokenR\x05token\x12+\n" + + "\x0fcloned_token_id\x18\x03 \x01(\tH\x00R\rclonedTokenId\x88\x01\x01B\x12\n" + + "\x10_cloned_token_id\"\x83\x01\n" + + "\x0eLandscapeToken\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06secret\x18\x02 \x01(\tR\x06secret\x12\x14\n" + + "\x05alias\x18\x03 \x01(\tR\x05alias\x12\x19\n" + + "\bowner_id\x18\x04 \x01(\tR\aownerId\x12\x18\n" + + "\acreated\x18\x05 \x01(\x04R\acreated*Y\n" + + "\tEventType\x12\v\n" + + "\aCREATED\x10\x00\x12\v\n" + + "\aDELETED\x10\x01\x12\n" + + "\n" + + "\x06CLONED\x10\x02\x12\x12\n" + + "\x0eACCESS_GRANTED\x10\x03\x12\x12\n" + + "\x0eACCESS_REVOKED\x10\x04B>Z EventType + 2, // 1: TokenEvent.token:type_name -> LandscapeToken + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_token_event_proto_init() } +func file_token_event_proto_init() { + if File_token_event_proto != nil { + return + } + file_token_event_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_token_event_proto_rawDesc), len(file_token_event_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_token_event_proto_goTypes, + DependencyIndexes: file_token_event_proto_depIdxs, + EnumInfos: file_token_event_proto_enumTypes, + MessageInfos: file_token_event_proto_msgTypes, + }.Build() + File_token_event_proto = out.File + file_token_event_proto_goTypes = nil + file_token_event_proto_depIdxs = nil +} diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go new file mode 100644 index 0000000..e0d3f7b --- /dev/null +++ b/internal/kafka/spanproc/spanproc.go @@ -0,0 +1,124 @@ +package spanproc + +import ( + "context" + "fmt" + "log/slog" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/ExplorViz/trace-service/internal/attrib" + "github.com/ExplorViz/trace-service/internal/conversion" + "github.com/ExplorViz/trace-service/internal/genproto/spanpb" + "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" + "github.com/twmb/franz-go/pkg/kgo" + "google.golang.org/protobuf/proto" + + coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" +) + +var lastReceivedSpans atomic.Uint64 +var lastInvalidSpans atomic.Uint64 + +func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc.TokenStore, logInterval time.Duration) { + spans := make(chan *attrib.SpanReader) + results := make(chan *spanpb.ParsedSpan) + + workerCount := runtime.NumCPU() + var wg sync.WaitGroup + for range workerCount { + wg.Go(func() { consumerWorker(ctx, spans, results, ts, validateTokens) }) + } + wg.Go(func() { producerWorker(ctx, results, cl) }) + + if logInterval > 0 { + go func() { + ticker := time.NewTicker(logInterval) + defer ticker.Stop() + + for range ticker.C { + count := lastReceivedSpans.Swap(0) + slog.Info(fmt.Sprintf("received %d spans", count)) + } + }() + } + + for { + fs := cl.PollFetches(ctx) + if ctx.Err() != nil { + slog.Debug("exiting kafka span poll loop") + break + } + fs.EachRecord(func(r *kgo.Record) { + var req coltracepb.ExportTraceServiceRequest + if err := proto.Unmarshal(r.Value, &req); err != nil { + slog.Debug("invalid protocol buffer for span", "error", err) + return + } + + for _, rs := range req.ResourceSpans { + for _, ss := range rs.ScopeSpans { + for _, s := range ss.Spans { + sr := attrib.NewSpanReader(s, ss.Scope, rs.Resource) + spans <- &sr + } + } + } + }) + } + + wg.Wait() +} + +func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, results chan<- *spanpb.ParsedSpan, ts *tokenproc.TokenStore, validateTokens bool) { + for { + select { + case <-ctx.Done(): + slog.Debug("exiting span consumer worker") + return + + case s := <-spans: + lastReceivedSpans.Add(1) + + if validateTokens { + tokenID := s.SpanAttribute(attrib.ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() + tokenSecret := s.SpanAttribute(attrib.ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() + if !ts.HasToken(tokenID, tokenSecret) { + slog.Debug("invalid span: unknown landscape token ID or incorrect secret", "landscapeTokenID", tokenID) + lastInvalidSpans.Add(1) + continue + } + } + + p, err := conversion.ConvertSpan(s) + if err != nil { + slog.Error("failed to convert span", "error", err) + continue + } + results <- conversion.ToProto(p) + } + } +} + +func producerWorker(ctx context.Context, results <-chan *spanpb.ParsedSpan, cl *kgo.Client) { + for { + select { + case <-ctx.Done(): + slog.Debug("exiting span producer worker") + return + + case s := <-results: + out, err := proto.Marshal(s) + if err != nil { + slog.Error("failed to encode protobuf", "error", err) + continue + } + cl.ProduceSync(ctx, &kgo.Record{ + Key: []byte(s.LandscapeTokenId), + Value: out, + }) + } + } +} diff --git a/internal/kafka/tokenproc/tokenproc.go b/internal/kafka/tokenproc/tokenproc.go new file mode 100644 index 0000000..1cddcf7 --- /dev/null +++ b/internal/kafka/tokenproc/tokenproc.go @@ -0,0 +1,73 @@ +// Package tokenproc is concerned with processing landscape token events, +// such as the creation of new tokens and the deletion of existing tokens. +// It provides means of keeping track of existing landscape tokens and react +package tokenproc + +import ( + "context" + "log/slog" + "sync" + + "github.com/ExplorViz/trace-service/internal/genproto/tokenpb" + "github.com/twmb/franz-go/pkg/kgo" + "google.golang.org/protobuf/proto" +) + +// A TokenStore keeps track of existing landscape tokens and their corresponding token secrets. +type TokenStore struct { + mu sync.Mutex + m map[string]string +} + +func NewTokenStore() TokenStore { + return TokenStore{ + m: make(map[string]string), + } +} + +func (s *TokenStore) put(id string, secret string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.m[id] = secret +} + +func (s *TokenStore) delete(id string) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.m, id) +} + +// HasToken reports whether the store knows about a landscape token +// with the given ID and secret +func (s *TokenStore) HasToken(id string, secret string) bool { + val, ok := s.m[id] + return ok && val == secret +} + +// Run continuously fetches records from the given client, attempts to deserialize them +// as [tokenpb.TokenEvent]s and updates the provided [TokenStore] accordingly. +func Run(ctx context.Context, cl *kgo.Client, ts *TokenStore) { + for { + fs := cl.PollFetches(ctx) + if ctx.Err() != nil { + slog.Debug("exiting kafka token poll loop") + break + } + fs.EachRecord(func(r *kgo.Record) { + if r.Value == nil { + ts.delete(string(r.Key)) + return + } + + var t tokenpb.TokenEvent + if err := proto.Unmarshal(r.Value, &t); err != nil { + slog.Debug("invalid protocol buffer for token event", "error", err) + return + } + + ts.put(string(r.Key), t.GetToken().GetSecret()) + }) + } +} diff --git a/internal/conversion/parsing/code.go b/internal/parsing/code.go similarity index 91% rename from internal/conversion/parsing/code.go rename to internal/parsing/code.go index b7ca48b..3d8bdec 100644 --- a/internal/conversion/parsing/code.go +++ b/internal/parsing/code.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/ExplorViz/trace-service/internal/attrib" semconv "go.opentelemetry.io/otel/semconv/v1.41.0" ) @@ -20,7 +21,7 @@ func (c CodeSpanEntity) Id() string { return c.FilePath + " " + c.FuncName + " " + c.ClassName + " " + c.GitCommitHash } -func ParseCodeSpan(s SpanReader) (SpanEntity, error) { +func ParseCodeSpan(s *attrib.SpanReader) (SpanEntity, error) { fqn := s.SpanAttribute(semconv.CodeFunctionNameKey).GetStringValue() lang := s.ResourceAttribute(semconv.TelemetrySDKLanguageKey).GetStringValue() diff --git a/internal/parsing/common.go b/internal/parsing/common.go new file mode 100644 index 0000000..720273a --- /dev/null +++ b/internal/parsing/common.go @@ -0,0 +1,9 @@ +package parsing + +import "github.com/ExplorViz/trace-service/internal/attrib" + +type SpanParser func(s *attrib.SpanReader) (SpanEntity, error) + +type SpanEntity interface { + Id() string +} diff --git a/internal/conversion/parsing/doc.go b/internal/parsing/doc.go similarity index 100% rename from internal/conversion/parsing/doc.go rename to internal/parsing/doc.go diff --git a/internal/conversion/parsing/fqn.go b/internal/parsing/fqn.go similarity index 100% rename from internal/conversion/parsing/fqn.go rename to internal/parsing/fqn.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..d2a25e5 --- /dev/null +++ b/main.go @@ -0,0 +1,98 @@ +//go:generate go run ./scripts/genproto.go + +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/ExplorViz/trace-service/internal/kafka/spanproc" + "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" + + "github.com/twmb/franz-go/pkg/kgo" +) + +var ( + seedBroker = flag.String("broker", "localhost:9091", "network endpoint of the Kafka broker to use") + inTopic = flag.String("topic-in", "telemetry.spans.raw", "Kafka topic to consume OpenTelemetry OTLP spans from") + outTopic = flag.String("topic-out", "telemetry.spans.parsed", "Kafka topic to produce parsed spans into") + tokensTopic = flag.String("topic-tokens", "tokens.events", "Kafka topic to consume landscape token events from") + validateTokens = flag.Bool("validate-tokens", false, "whether to verify the existence of provided landscape tokens for incoming traces") + logInterval = flag.Duration("log-interval", 5*time.Second, "interval at which received spans should be logged (0 to disable)") +) + +func main() { + slog.SetLogLoggerLevel(slog.LevelDebug) + + flag.Parse() + if *logInterval < 0 { + slog.Error("span logging duration must be positive") + flag.Usage() + os.Exit(1) + } + + spanCl, err := kgo.NewClient( + kgo.SeedBrokers(*seedBroker), + kgo.ConsumeTopics(*inTopic), + kgo.DefaultProduceTopic(*outTopic), + kgo.ConsumerGroup("trace-service"), + ) + if err != nil { + slog.Error("unable to initialize kgo client for spans", "error", err) + os.Exit(1) + } + defer spanCl.Close() + + tokenCl, err := kgo.NewClient( + kgo.SeedBrokers(*seedBroker), + kgo.ConsumeTopics(*tokensTopic), + kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()), // replay token events from beginning + ) + if err != nil { + slog.Error("unable to initialize kgo client for token events", "error", err) + os.Exit(1) + } + defer tokenCl.Close() + + ctx, cancel := context.WithCancel(context.Background()) + sigs := make(chan os.Signal, 2) + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigs + slog.Info("received interrupt signal; gracefully stopping ...") + cancel() + <-sigs + slog.Info("received second interrupt signal; exiting immediately") + os.Exit(1) + }() + + var wg sync.WaitGroup + ts := tokenproc.NewTokenStore() + wg.Go(func() { spanproc.Run(ctx, spanCl, *validateTokens, &ts, *logInterval) }) + + if *validateTokens { + wg.Go(func() { tokenproc.Run(ctx, tokenCl, &ts) }) + } + + fmt.Print(` + ______ _ __ ___ + | ____| | | \ \ / (_) + | |__ __ ___ __ | | ___ _ _\ \ / / _ ____ + | __| \ \/ / '_ \| |/ _ \| '__\ \/ / | |_ / + | |____ > <| |_) | | (_) | | \ / | |/ / + |______/_/\_\ .__/|_|\___/|_| \/ |_/___| + | | + |_| trace-service + +`) + + <-ctx.Done() + wg.Wait() +} diff --git a/proto/persistence_span.proto b/proto/parsed_span.proto similarity index 96% rename from proto/persistence_span.proto rename to proto/parsed_span.proto index 562ab34..a3083da 100644 --- a/proto/persistence_span.proto +++ b/proto/parsed_span.proto @@ -2,7 +2,7 @@ syntax = "proto3"; option go_package = "github.com/ExplorViz/trace-service/internal/genproto/spanpb"; -message SpanData { +message ParsedSpan { string landscape_token_id = 1; string landscape_token_secret = 2; diff --git a/proto/token_event.proto b/proto/token_event.proto new file mode 100644 index 0000000..abc3354 --- /dev/null +++ b/proto/token_event.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +option go_package = "github.com/ExplorViz/trace-service/internal/genproto/tokenpb"; + +message TokenEvent { + EventType type = 1; + LandscapeToken token = 2; + optional string cloned_token_id = 3; +} + +enum EventType { + CREATED = 0; + DELETED = 1; + CLONED = 2; + ACCESS_GRANTED = 3; + ACCESS_REVOKED = 4; +} + +message LandscapeToken { + string id = 1; + string secret = 2; + string alias = 3; + string owner_id = 4; + uint64 created = 5; +} \ No newline at end of file diff --git a/scripts/genproto.go b/scripts/genproto.go new file mode 100644 index 0000000..8063cba --- /dev/null +++ b/scripts/genproto.go @@ -0,0 +1,40 @@ +//go:build ignore + +// Command genproto generates the Go code required for this application from the .proto files +// within the proto directory of this repository. This is achieved by calling the protoc compiler, +// which must be installed for this script to work. +// +// The purpose of this dedicated script is to provide a cross-platform way to invoke the protoc command. +// As such, this command is not intended to be run directly and should instead be invoked using go:generate. +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +func main() { + files, err := filepath.Glob("./proto/*.proto") + if err != nil { + fmt.Printf("error matching glob pattern: %v\n", err) + os.Exit(1) + } + + if len(files) == 0 { + fmt.Println("no matching files found") + return + } + + args := append([]string{"-I=./proto", "--go_out=.", "--go_opt=module=github.com/ExplorViz/trace-service"}, files...) + + cmd := exec.Command("protoc", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + fmt.Printf("protobuf generation failed: %v\n", err) + os.Exit(1) + } +} From 20e600a66dbb55cd30ec1399fd2ba25f1247ca55 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Mon, 15 Jun 2026 10:30:42 +0200 Subject: [PATCH 07/27] Update .gitignore --- .gitignore | 342 +++++------------------------------------------------ 1 file changed, 32 insertions(+), 310 deletions(-) diff --git a/.gitignore b/.gitignore index 2c1696e..905d0c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,310 +1,32 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/intellij,eclipse,java,gradle,macos,windows,linux -# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,eclipse,java,gradle,macos,windows,linux - -target -.idea - -### Eclipse ### -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# CDT- autotools -.autotools - -# Java annotation processor (APT) -.factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Annotation Processing -.apt_generated/ -.apt_generated_test/ - -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - -# Uncomment this line if you wish to ignore the project description file. -# Typically, this file would be tracked if it contains build/dependency configurations: -#.project - -### Eclipse Patch ### -# Spring Boot Tooling -.sts4-cache/ - -### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf -*.hprof - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -### Java ### -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -### Gradle ### -.gradle -**/build/ -!src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Avoid ignore Gradle wrappper properties -!gradle-wrapper.properties - -# Cache of project -.gradletasknamecache - -# Eclipse Gradle plugin generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - -# End of https://www.toptal.com/developers/gitignore/api/intellij,eclipse,java,gradle,macos,windows,linux - -# Quarkus -.quarkus - -# Kotlin -.kotlin - +# Based on: +# https://github.com/github/gitignore/blob/main/Go.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ + +# Project-specific +trace-service \ No newline at end of file From eee339ffa1fb77af8cc7a6aaeb577fa9a0ab1be1 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Mon, 15 Jun 2026 10:30:55 +0200 Subject: [PATCH 08/27] Update .editorconfig --- .editorconfig | 478 +++----------------------------------------------- 1 file changed, 21 insertions(+), 457 deletions(-) diff --git a/.editorconfig b/.editorconfig index c165dbb..1fea6cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,466 +1,30 @@ +root = true + [*] +insert_final_newline = true charset = utf-8 -end_of_line = lf -indent_size = 2 +trim_trailing_whitespace = true indent_style = space -insert_final_newline = true -max_line_length = 100 -tab_width = 2 -ij_continuation_indent_size = 4 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = true -ij_smart_tabs = false -ij_visual_guides = none -ij_wrap_on_typing = false - -[*.java] -tab_width = 4 -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = false -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = false -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = false -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_align_types_in_multi_catch = false -ij_java_annotation_parameter_wrap = normal -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = normal -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = normal -ij_java_binary_operation_sign_on_next_line = true -ij_java_binary_operation_wrap = normal -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 2 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 0 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_add_space = false -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = normal -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = split_into_lines -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 999 -ij_java_class_names_in_javadoc = 1 -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_not_wrap_after_single_annotation_in_parameter = false -ij_java_do_while_brace_force = always -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = false -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = true -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = true -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = false -ij_java_entity_dd_suffix = EJB -ij_java_entity_eb_suffix = Bean -ij_java_entity_hi_suffix = Home -ij_java_entity_lhi_prefix = Local -ij_java_entity_lhi_suffix = Home -ij_java_entity_li_prefix = Local -ij_java_entity_pk_class = java.lang.String -ij_java_entity_vo_suffix = VO -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = normal -ij_java_extends_list_wrap = normal -ij_java_field_annotation_wrap = split_into_lines -ij_java_finally_on_new_line = false -ij_java_for_brace_force = always -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = normal -ij_java_generate_final_locals = true -ij_java_generate_final_parameters = true -ij_java_if_brace_force = always -ij_java_imports_layout = $*,|,* -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = true -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 3 -ij_java_keep_blank_lines_between_package_declaration_and_header = 3 -ij_java_keep_blank_lines_in_code = 3 -ij_java_keep_blank_lines_in_declarations = 3 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = false -ij_java_keep_first_column_comment = false -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_add_space_on_reformat = false -ij_java_line_comment_at_first_column = true -ij_java_message_dd_suffix = EJB -ij_java_message_eb_suffix = Bean -ij_java_method_annotation_wrap = split_into_lines -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = normal -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = normal -ij_java_modifier_list_wrap = false -ij_java_multi_catch_types_wrap = normal -ij_java_names_count_to_use_import_on_demand = 999 -ij_java_new_line_after_lparen_in_annotation = false -ij_java_new_line_after_lparen_in_record_header = false -ij_java_parameter_annotation_wrap = normal -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = on_every_item -ij_java_rparen_on_new_line_in_annotation = false -ij_java_rparen_on_new_line_in_record_header = false -ij_java_session_dd_suffix = EJB -ij_java_session_eb_suffix = Bean -ij_java_session_hi_suffix = Home -ij_java_session_lhi_prefix = Local -ij_java_session_lhi_suffix = Home -ij_java_session_li_prefix = Local -ij_java_session_si_suffix = Service -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = true -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_annotation_eq = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = true -ij_java_ternary_operation_wrap = on_every_item -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = normal -ij_java_throws_list_wrap = normal -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = split_into_lines -ij_java_visibility = public -ij_java_while_brace_force = always -ij_java_while_on_new_line = false -ij_java_wrap_comments = true -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false +indent_size = 2 +end_of_line = lf -[{*.kt,*.kts}] -indent_style = space -insert_final_newline = true -max_line_length = 120 +[{Makefile,*.go,.gitmodules}] +indent_style = tab indent_size = 4 -ij_continuation_indent_size = 4 -ij_java_names_count_to_use_import_on_demand = 9999 -ij_kotlin_align_in_columns_case_branch = false -ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = false -ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = true -ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true -ij_kotlin_assignment_wrap = normal -ij_kotlin_blank_lines_after_class_header = 0 -ij_kotlin_blank_lines_around_block_when_branches = 0 -ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = true -ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = on_every_item -ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL -ij_kotlin_continuation_indent_for_chained_calls = true -ij_kotlin_continuation_indent_for_expression_bodies = true -ij_kotlin_continuation_indent_in_argument_lists = true -ij_kotlin_continuation_indent_in_elvis = false -ij_kotlin_continuation_indent_in_if_conditions = false -ij_kotlin_continuation_indent_in_parameter_lists = false -ij_kotlin_continuation_indent_in_supertype_lists = false -ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = off -ij_kotlin_finally_on_new_line = false -ij_kotlin_if_rparen_on_new_line = false -ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = * -ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 2 -ij_kotlin_keep_blank_lines_in_code = 2 -ij_kotlin_keep_blank_lines_in_declarations = 2 -ij_kotlin_keep_first_column_comment = true -ij_kotlin_keep_indents_on_empty_lines = false -ij_kotlin_keep_line_breaks = true -ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_comment_add_space = false -ij_kotlin_line_comment_at_first_column = true -ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = true -ij_kotlin_method_parameters_right_paren_on_new_line = true -ij_kotlin_method_parameters_wrap = on_every_item -ij_kotlin_name_count_to_use_star_import = 9999 -ij_kotlin_name_count_to_use_star_import_for_members = 9999 -ij_kotlin_parameter_annotation_wrap = off -ij_kotlin_space_after_comma = true -ij_kotlin_space_after_extend_colon = true -ij_kotlin_space_after_type_colon = true -ij_kotlin_space_before_catch_parentheses = true -ij_kotlin_space_before_comma = false -ij_kotlin_space_before_extend_colon = true -ij_kotlin_space_before_for_parentheses = true -ij_kotlin_space_before_if_parentheses = true -ij_kotlin_space_before_lambda_arrow = true -ij_kotlin_space_before_type_colon = false -ij_kotlin_space_before_when_parentheses = true -ij_kotlin_space_before_while_parentheses = true -ij_kotlin_spaces_around_additive_operators = true -ij_kotlin_spaces_around_assignment_operators = true -ij_kotlin_spaces_around_equality_operators = true -ij_kotlin_spaces_around_function_type_arrow = true -ij_kotlin_spaces_around_logical_operators = true -ij_kotlin_spaces_around_multiplicative_operators = true -ij_kotlin_spaces_around_range = false -ij_kotlin_spaces_around_relational_operators = true -ij_kotlin_spaces_around_unary_operator = false -ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_variable_annotation_wrap = off -ij_kotlin_while_on_new_line = false -ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_continuation_indent_size = 2 -ij_xml_align_attributes = false -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_add_space = false -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = true -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = false -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = true +[go.{mod,work,work.sum,sum}] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset +indent_size = unset -[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] -ij_json_array_wrapping = split_into_lines -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_keep_trailing_comma = false -ij_json_object_wrapping = split_into_lines -ij_json_property_alignment = do_not_align -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = false -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_add_space = false -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal - -[{*.markdown,*.md}] +[*.md] indent_size = 4 -tab_width = 4 -ij_continuation_indent_size = 8 -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_insert_quote_arrows_on_wrap = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_keep_line_breaks_inside_text_blocks = true -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 -ij_markdown_wrap_text_if_long = true -ij_markdown_wrap_text_inside_blockquotes = true +trim_trailing_whitespace = false + +eclint_indent_style = unset -[{*.yaml,*.yml}] -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true +[Dockerfile] +indent_size = 4 \ No newline at end of file From 05e00852ef2bca3eb9f3c1dfc2a34080e7853307 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Mon, 15 Jun 2026 10:31:14 +0200 Subject: [PATCH 09/27] Create Makefile, update README --- Makefile | 12 ++++++++++ README.md | 72 ++++++++++++++++++++++++------------------------------- 2 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..61b0f87 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: all generate build run + +all: generate build + +generate: + go generate + +build: generate + go build + +run: generate + @go run . || true \ No newline at end of file diff --git a/README.md b/README.md index 3e7313d..348f17b 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,53 @@ -# ExplorViz trace-service +# trace-service -Scalable service that processes, persists, aggregates and queries the observed traces of method executions within -monitored software applications. +The trace-service is a scalable service that processes, interprets, persists, and queries [OpenTelemetry execution traces](https://opentelemetry.io/docs/concepts/signals/traces/) within monitored software applications. It attempts to classify the entities described by incoming spans for the purpose of visualization. + +Consumes traces sent via Kafka from an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) instance (defined in the [Deployment](https://git.se.informatik.uni-kiel.de/ExplorViz/code/deployment)). Processed spans are produced to a Kafka topic for the [Landscape Service](https://git.se.informatik.uni-kiel.de/ExplorViz/code/landscape-service) to consume. Interaction with Kafka is implemented using the [franz-go](https://github.com/twmb/franz-go) library. ## Prerequisites -- Java 17 or higher +- Go 1.25.10 or higher +- A code editor, such as [Visual Studio Code](https://code.visualstudio.com/) - Make sure to run the [ExplorViz software stack](https://git.se.informatik.uni-kiel.de/ExplorViz/code/deployment) before starting the service, as it provides the required database(s) and the Kafka broker -## Running the application in dev mode +## Installing dependencies -You can run your application in dev mode that enables live coding using: -```shell script -./gradlew quarkusDev +Before building the service, install the dependencies declared in the `go.mod` using: +```shell +go get ./.. ``` -This also enables the `dev` configuration profile, i.e. using the properties prefixed with `%dev` from -`src/main/resources/application.properties`. - -**_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. - -## Packaging and running the application +## Running the service -The application can be packaged and tested using: -```shell script -./gradlew build +You can run the service using: +```shell +go run . [OPTIONS] ``` -It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +To see a list of command-line options, use the `--help` flag. These options can also be configured via environment variables. -You can skip running the integration tests by adding `-x integrationTest`. To skip all tests and code analysis use the -`assemble` task instead of `build`. +## Building an executable -The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. -Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. - -If you want to build an _über-jar_, which includes the entire application in a single jar file, execute the following -command: -```shell script -./gradlew build -Dquarkus.package.type=uber-jar +To build an executable from the project, use: +```shell +go build ``` -The application, packaged as an _über-jar_, is now runnable using -`java -jar build/trace-service-1.0-SNAPSHOT-runner.jar`. -You can add `-Dquarkus.profile=dev` to enable the `%dev` properties. +By default, the executable will be placed in the root directory under the name `trace-service`. You can optionally specify the path of the resulting binary using the `-o ` flag. -## Creating a native executable +## Compiling Protobuf -You can create a native executable using: -```shell script -./gradlew build -Dquarkus.package.type=native +When updating any `.proto` files, make sure to compile the Protobuf files to Go using: +```shell +go generate ``` -Or, if you don't have GraalVM installed, you can run the native executable build in a container using: -```shell script -./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +Alternatively, you can use the provided Makefile to compile the Protobuf and build the project in a single step: +```shell +make ``` -You can then execute your native executable with: `./build/trace-service-1.0-SNAPSHOT-runner` - -If you want to learn more about building native executables, please consult -https://quarkus.io/guides/gradle-tooling#building-a-native-executable. +If you just want to run the project, use: +```shell +make run +``` \ No newline at end of file From e4fa9d690a62c4b1ad81c9b1364fb1cd014c8110 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Mon, 15 Jun 2026 19:16:39 +0200 Subject: [PATCH 10/27] Introduce ff flag parsing library for handling environment variable --- README.md | 2 +- go.mod | 1 + go.sum | 6 ++++++ main.go | 36 +++++++++++++++++++++++++----------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 348f17b..d86d18b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ You can run the service using: ```shell go run . [OPTIONS] ``` -To see a list of command-line options, use the `--help` flag. These options can also be configured via environment variables. +To see a list of command-line options, use the `--help` flag. These options can also be configured via environment variables, where the name of the environment variable corresponds to the long flag name, prefixed by `EXPLORVIZ_` and with all separators replaced by underscores; for example, the `--log-level` flag corresponds to the `EXPLORVIZ_LOG_LEVEL` environment variable. Note that directly passing flags takes precedence over environment variables. If neither the flag nor the environment variable is set, then the default value indicated by `--help` is used. ## Building an executable diff --git a/go.mod b/go.mod index 4c8b265..b225236 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ExplorViz/trace-service go 1.25.10 require ( + github.com/peterbourgon/ff/v4 v4.0.0-beta.1 github.com/twmb/franz-go v1.21.3 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/proto/otlp v1.10.0 diff --git a/go.sum b/go.sum index 919e655..51ed346 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc= +github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -58,5 +62,7 @@ google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index d2a25e5..6c86b69 100644 --- a/main.go +++ b/main.go @@ -15,28 +15,42 @@ import ( "github.com/ExplorViz/trace-service/internal/kafka/spanproc" "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" + "github.com/peterbourgon/ff/v4" + "github.com/peterbourgon/ff/v4/ffhelp" "github.com/twmb/franz-go/pkg/kgo" ) -var ( - seedBroker = flag.String("broker", "localhost:9091", "network endpoint of the Kafka broker to use") - inTopic = flag.String("topic-in", "telemetry.spans.raw", "Kafka topic to consume OpenTelemetry OTLP spans from") - outTopic = flag.String("topic-out", "telemetry.spans.parsed", "Kafka topic to produce parsed spans into") - tokensTopic = flag.String("topic-tokens", "tokens.events", "Kafka topic to consume landscape token events from") - validateTokens = flag.Bool("validate-tokens", false, "whether to verify the existence of provided landscape tokens for incoming traces") - logInterval = flag.Duration("log-interval", 5*time.Second, "interval at which received spans should be logged (0 to disable)") -) - func main() { - slog.SetLogLoggerLevel(slog.LevelDebug) + fs := ff.NewFlagSet("trace-service") + var ( + seedBroker = fs.String('b', "broker", "localhost:9091", "network endpoint of the Kafka broker to use") + inTopic = fs.String('i', "topic-in", "telemetry.spans.raw", "Kafka topic to consume OpenTelemetry OTLP spans from") + outTopic = fs.String('o', "topic-out", "telemetry.spans.parsed", "Kafka topic to produce parsed spans into") + tokensTopic = fs.String('t', "topic-tokens", "tokens.events", "Kafka topic to consume landscape token events from") + validateTokens = fs.BoolLong("validate-tokens", "whether to verify the existence of provided landscape tokens for incoming traces") + logLevel = fs.StringEnum('l', "log-level", "log level: info, error, debug", "info", "error", "debug") + logInterval = fs.DurationLong("log-interval", 5*time.Second, "interval at which received spans should be logged (0 to disable)") + ) + if err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("EXPLORVIZ")); err != nil { + fmt.Println(err) + fmt.Printf("%s\n", ffhelp.Flags(fs)) + os.Exit(0) + } - flag.Parse() if *logInterval < 0 { slog.Error("span logging duration must be positive") flag.Usage() os.Exit(1) } + switch *logLevel { + case "info": + slog.SetLogLoggerLevel(slog.LevelInfo) + case "error": + slog.SetLogLoggerLevel(slog.LevelError) + case "debug": + slog.SetLogLoggerLevel(slog.LevelDebug) + } spanCl, err := kgo.NewClient( kgo.SeedBrokers(*seedBroker), From ad8a71fa88686c0acfe4b880a382f8a9cf258485 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:17:21 +0200 Subject: [PATCH 11/27] Update CI/CD for Go --- .github/workflows/ci.yaml | 60 ++++++++++++ .gitlab-ci.yml | 93 +++++++++--------- .golangci.yml | 20 ++++ buf.yaml | 4 + workflows/ci.yaml | 174 ---------------------------------- workflows/release-version.yml | 23 ----- 6 files changed, 132 insertions(+), 242 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .golangci.yml create mode 100644 buf.yaml delete mode 100644 workflows/ci.yaml delete mode 100644 workflows/release-version.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..cbffff6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,60 @@ +name: CI Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + +env: + GO_VERSION: "1.26.x" + GOLANGCI_LINT_VERSION: "v2.12" + BUF_VERSION: "1.70.0" + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + - name: Install dependencies + run: go get ./... + - name: Build + run: go build -v ./... + - name: Test + run: go test -v -race + + lint: + name: Code Lint + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: ${{ env.GOLANGCI_LINT_VERSION }} + + buf: + name: Protobuf Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: bufbuild/buf-action@v1 + with: + version: ${{ env.BUF_VERSION }} + breaking: false + push: false + archive: false + pr_comment: false diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e3ea0da..6bf0dc2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,19 +2,14 @@ include: - project: "ExplorViz/code/deployment" file: ".gitlab-ci-explorviz-common.yml" +variables: + GO_DOCKER_VERSION: "1.26" + GOLANGCI_LINT_VERSION: "v2.12" + BUF_VERSION: "1.71" + default: tags: - exec-docker - image: ghcr.io/graalvm/graalvm-community:21 - cache: # Cache modules in between jobs - key: $CI_COMMIT_REF_SLUG - paths: - - .gradle - before_script: - - export GRADLE_USER_HOME=`pwd`/.gradle - -variables: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" workflow: rules: @@ -29,54 +24,62 @@ stages: - test - deploy:images +# See https://docs.gitlab.com/ci/caching/examples/#go +.go-cache: + variables: + GOPATH: $CI_PROJECT_DIR/.go + GOCACHE: "$CI_PROJECT_DIR/.cache/go-build" + before_script: + - mkdir -p .go .cache/go-build + cache: + key: + files: + - go.mod + - go.sum + paths: + - .go/pkg/mod/ + - .cache/go-build/ + build: + image: "golang:${GO_DOCKER_VERSION}" + extends: .go-cache stage: build - script: ./gradlew assemble + script: + - gogo build -o trace-service . artifacts: - paths: - - build + paths: ./trace-service -pmd: +code-lint: + image: "golangci/golangci-lint:${GOLANGCI_LINT_VERSION}" + extends: .go-cache stage: code-analysis - script: ./gradlew pmdMain - artifacts: - paths: - - build - -checkstyle: - stage: code-analysis - script: ./gradlew checkstyleMain + script: + - golangci-lint run ./... --timeout=5m --output.code-climate.path=gl-code-quality-report.json artifacts: - paths: - - build + reports: + codequality: gl-code-quality-report.json -unit-test: - stage: test +proto-lint: + image: "bufbuild/buf:${BUF_VERSION}" + rules: + - changes: + - "**/*.proto" + - ./proto/buf.yaml script: - - ./gradlew test - artifacts: - paths: - - build + - buf lint -integration-test: +test: + image: "golang:${GO_DOCKER_VERSION}" + extends: .go-cache stage: test script: - - ./gradlew integrationTest - allow_failure: true - artifacts: - paths: - - build - -push-dockerhub-native-mr: - extends: .build-and-deploy-quarkus-native - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - variables: - IMAGE_TAG: mr-${CI_MERGE_REQUEST_IID}-native + - go test -v -race ./... push-dockerhub-mr: - extends: .build-and-deploy-quarkus-jvm + extends: .build-and-deploy rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: - IMAGE_TAG: mr-${CI_MERGE_REQUEST_IID}-jvm + DOCKERHUB_IMAGE_NAME: "trace-service" + + IMAGE_TAG: mr-${CI_MERGE_REQUEST_IID} diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a0338a7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +version: "2" + +linters: + default: none + + enable: + - errcheck + - govet + - protogetter + - staticcheck + - unused + +formatters: + enable: + - goimports + + settings: + goimports: + local-prefixes: + - github.com/ExplorViz/trace-service diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..98b5b0d --- /dev/null +++ b/buf.yaml @@ -0,0 +1,4 @@ +# Buf is used for CI/CD linting of .proto files +version: v2 +modules: + -path: . \ No newline at end of file diff --git a/workflows/ci.yaml b/workflows/ci.yaml deleted file mode 100644 index 3227fba..0000000 --- a/workflows/ci.yaml +++ /dev/null @@ -1,174 +0,0 @@ -name: CI Pipeline - -on: - push: - branches: - - main - pull_request: - branches: - - "**" - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Build Project - run: ./gradlew assemble - env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" - - pmd: - name: PMD Analysis - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run PMD Analysis - run: ./gradlew pmdMain - - checkstyle: - name: Checkstyle Analysis - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run Checkstyle - run: ./gradlew checkstyleMain - - unit-test: - name: Unit Tests - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run Unit Tests - run: ./gradlew test - - integration-test: - name: Integration Tests - runs-on: ubuntu-latest - needs: build - # Do not run since integration tests are not working - if: false - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: "graalvm" - java-version: "21" - - name: Cache Gradle Packages - uses: actions/cache@v4 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Run Integration Tests - run: ./gradlew integrationTest - - uses: actions/upload-artifact@v4 # upload test results - if: success() || failure() # run this step even if previous step failed - with: - name: test-results-integration - path: build/reports/tests/integrationTest/index.html - - # Combine test reports if needed - test-report: - name: Test Report - runs-on: ubuntu-latest - # Do not run since integration tests are not working - if: false - needs: [unit-test, integration-test] - steps: - - uses: dorny/test-reporter@v1 - with: - artifact: /test-results-(.*)/ # artifact name - name: java-junit # Name of the check run which will be created - path: "*.html" # Path to test results (inside artifact .zip) - reporter: jest-junit # Format of test results - - test: - name: test end node - runs-on: ubuntu-latest - needs: [build, pmd, checkstyle, unit-test] - steps: - - run: echo "finished test stage" # this is a indirection to the previous test steps - - docker-image-native: - name: Docker image native - if: github.event_name == 'push' && github.ref_name == 'main' - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Build and Push Image - uses: explorviz/deployment/.github/actions/build-and-deploy-quarkus-native@main - with: - platforms: "linux/amd64,linux/arm64/v8" - docker-username: ${{ secrets.DOCKER_USERNAME }} - docker-password: ${{ secrets.DOCKER_PASSWORD }} - image-name: ${{ vars.DOCKER_NATIVE_IMAGE_NAME }} - image-tag: "latest" - - docker-image-jvm: - name: Docker image JVM - if: github.event_name == 'push' && github.ref_name == 'main' - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Build and Push Image - uses: explorviz/deployment/.github/actions/build-and-deploy-quarkus-jvm@main - with: - platforms: "linux/amd64,linux/arm64/v8" - docker-username: ${{ secrets.DOCKER_USERNAME }} - docker-password: ${{ secrets.DOCKER_PASSWORD }} - image-name: ${{ vars.DOCKER_JVM_IMAGE_NAME }} - image-tag: "latest" diff --git a/workflows/release-version.yml b/workflows/release-version.yml deleted file mode 100644 index dfa41e5..0000000 --- a/workflows/release-version.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Release Tagged Docker Image - -on: - push: - tags: - - "*" - workflow_dispatch: - -jobs: - release-docker-image-jvm: - name: Release Tagged Docker Image - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - name: Build and Push Image - uses: explorviz/deployment/.github/actions/build-and-deploy-quarkus-jvm@main - with: - platforms: "linux/amd64,linux/arm64/v8" - docker-username: ${{ secrets.DOCKER_USERNAME }} - docker-password: ${{ secrets.DOCKER_PASSWORD }} - image-name: ${{ vars.DOCKER_JVM_IMAGE_NAME }} - image-tag: "${{ github.ref_name }}" From 8f9ba33bc5599243e2d79c31b68a0a3b91864463 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:17:47 +0200 Subject: [PATCH 12/27] Update README for golangci-lint --- README.md | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d86d18b..2cc4d80 100644 --- a/README.md +++ b/README.md @@ -2,52 +2,67 @@ The trace-service is a scalable service that processes, interprets, persists, and queries [OpenTelemetry execution traces](https://opentelemetry.io/docs/concepts/signals/traces/) within monitored software applications. It attempts to classify the entities described by incoming spans for the purpose of visualization. -Consumes traces sent via Kafka from an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) instance (defined in the [Deployment](https://git.se.informatik.uni-kiel.de/ExplorViz/code/deployment)). Processed spans are produced to a Kafka topic for the [Landscape Service](https://git.se.informatik.uni-kiel.de/ExplorViz/code/landscape-service) to consume. Interaction with Kafka is implemented using the [franz-go](https://github.com/twmb/franz-go) library. +Consumes traces sent via Kafka from an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) instance (defined in the [Deployment](../deployment)). Processed spans are produced to a Kafka topic for the [Landscape Service](../landscape-service) to consume. Interaction with Kafka is implemented using the [franz-go](https://github.com/twmb/franz-go) library. -## Prerequisites +For development instructions, continue reading below. If you just want to run ExplorViz locally, refer to our [Deployment repository](../deployment) instead. + +## Development Instructions + +### Prerequisites - Go 1.25.10 or higher - A code editor, such as [Visual Studio Code](https://code.visualstudio.com/) -- Make sure to run the [ExplorViz software stack](https://git.se.informatik.uni-kiel.de/ExplorViz/code/deployment) +- Make sure to run the [ExplorViz software stack](../deployment) before starting the service, as it provides the required database(s) and the Kafka broker -## Installing dependencies - -Before building the service, install the dependencies declared in the `go.mod` using: -```shell -go get ./.. -``` - -## Running the service +### Running the service You can run the service using: + ```shell go run . [OPTIONS] ``` + To see a list of command-line options, use the `--help` flag. These options can also be configured via environment variables, where the name of the environment variable corresponds to the long flag name, prefixed by `EXPLORVIZ_` and with all separators replaced by underscores; for example, the `--log-level` flag corresponds to the `EXPLORVIZ_LOG_LEVEL` environment variable. Note that directly passing flags takes precedence over environment variables. If neither the flag nor the environment variable is set, then the default value indicated by `--help` is used. -## Building an executable +### Building an executable To build an executable from the project, use: + ```shell go build ``` By default, the executable will be placed in the root directory under the name `trace-service`. You can optionally specify the path of the resulting binary using the `-o ` flag. -## Compiling Protobuf +### Testing + +Be sure to write tests for new code and ensure that existing tests pass. You can run all tests using: + +```shell +go test ./... +``` + +### Compiling Protobuf When updating any `.proto` files, make sure to compile the Protobuf files to Go using: + ```shell go generate ``` Alternatively, you can use the provided Makefile to compile the Protobuf and build the project in a single step: + ```shell make ``` -If you just want to run the project, use: +If you just want to run the project while also compiling the Protobuf, use: + ```shell make run -``` \ No newline at end of file +``` + +### Code Style + +As part of our CI/CD pipeline, your code is linted and checked for formatting using [golangci-lint](https://github.com/golangci/golangci-lint), which you can also install locally to lint your code yourself prior to pushing. We recommend using the [official Visual Studio Code extension for Go](https://marketplace.visualstudio.com/items?itemName=golang.go) as well as [configuring the extension for golangci-lint](https://golangci-lint.run/docs/welcome/integrations/) to detect and fix linting / formatting issues as you're working. From 02f4c6f33086c0dde41435c6b97c9b37cd075253 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:18:21 +0200 Subject: [PATCH 13/27] Reformat and fixes for golangci-lint --- internal/attrib/spanreader.go | 16 ++++++++-------- internal/conversion/span.go | 3 ++- internal/kafka/spanproc/spanproc.go | 15 ++++++++------- internal/kafka/tokenproc/tokenproc.go | 5 ++++- internal/parsing/code.go | 3 ++- main.go | 6 +++--- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/internal/attrib/spanreader.go b/internal/attrib/spanreader.go index b5d8f39..5cde429 100644 --- a/internal/attrib/spanreader.go +++ b/internal/attrib/spanreader.go @@ -21,6 +21,14 @@ type SpanReader struct { resourceAttributes map[string]*commonpb.AnyValue } +func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resourcepb.Resource) SpanReader { + return SpanReader{ + spanAttributes: attrsToMap(s.GetAttributes()), + scopeAttributes: attrsToMap(sc.GetAttributes()), + resourceAttributes: attrsToMap(rs.GetAttributes()), + } +} + func (s SpanReader) SpanAttribute(key attribute.Key) *commonpb.AnyValue { return s.spanAttributes[string(key)] } @@ -33,14 +41,6 @@ func (s SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { return s.resourceAttributes[string(key)] } -func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resourcepb.Resource) SpanReader { - return SpanReader{ - spanAttributes: attrsToMap(s.Attributes), - scopeAttributes: attrsToMap(sc.Attributes), - resourceAttributes: attrsToMap(rs.Attributes), - } -} - func attrsToMap(attrs []*commonpb.KeyValue) map[string]*commonpb.AnyValue { m := make(map[string]*commonpb.AnyValue, len(attrs)) for _, kv := range attrs { diff --git a/internal/conversion/span.go b/internal/conversion/span.go index aad0726..9936cff 100644 --- a/internal/conversion/span.go +++ b/internal/conversion/span.go @@ -4,9 +4,10 @@ import ( "errors" "log/slog" + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" + "github.com/ExplorViz/trace-service/internal/attrib" "github.com/ExplorViz/trace-service/internal/parsing" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" ) type ParsedSpan struct { diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go index e0d3f7b..c541413 100644 --- a/internal/kafka/spanproc/spanproc.go +++ b/internal/kafka/spanproc/spanproc.go @@ -9,12 +9,13 @@ import ( "sync/atomic" "time" + "github.com/twmb/franz-go/pkg/kgo" + "google.golang.org/protobuf/proto" + "github.com/ExplorViz/trace-service/internal/attrib" "github.com/ExplorViz/trace-service/internal/conversion" "github.com/ExplorViz/trace-service/internal/genproto/spanpb" "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" - "github.com/twmb/franz-go/pkg/kgo" - "google.golang.org/protobuf/proto" coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" ) @@ -58,10 +59,10 @@ func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc return } - for _, rs := range req.ResourceSpans { - for _, ss := range rs.ScopeSpans { - for _, s := range ss.Spans { - sr := attrib.NewSpanReader(s, ss.Scope, rs.Resource) + for _, rs := range req.GetResourceSpans() { + for _, ss := range rs.GetScopeSpans() { + for _, s := range ss.GetSpans() { + sr := attrib.NewSpanReader(s, ss.GetScope(), rs.GetResource()) spans <- &sr } } @@ -116,7 +117,7 @@ func producerWorker(ctx context.Context, results <-chan *spanpb.ParsedSpan, cl * continue } cl.ProduceSync(ctx, &kgo.Record{ - Key: []byte(s.LandscapeTokenId), + Key: []byte(s.GetLandscapeTokenId()), Value: out, }) } diff --git a/internal/kafka/tokenproc/tokenproc.go b/internal/kafka/tokenproc/tokenproc.go index 1cddcf7..a506ed6 100644 --- a/internal/kafka/tokenproc/tokenproc.go +++ b/internal/kafka/tokenproc/tokenproc.go @@ -8,9 +8,10 @@ import ( "log/slog" "sync" - "github.com/ExplorViz/trace-service/internal/genproto/tokenpb" "github.com/twmb/franz-go/pkg/kgo" "google.golang.org/protobuf/proto" + + "github.com/ExplorViz/trace-service/internal/genproto/tokenpb" ) // A TokenStore keeps track of existing landscape tokens and their corresponding token secrets. @@ -58,6 +59,7 @@ func Run(ctx context.Context, cl *kgo.Client, ts *TokenStore) { fs.EachRecord(func(r *kgo.Record) { if r.Value == nil { ts.delete(string(r.Key)) + slog.Debug("received token delete event", "tokenID", r.Key) return } @@ -68,6 +70,7 @@ func Run(ctx context.Context, cl *kgo.Client, ts *TokenStore) { } ts.put(string(r.Key), t.GetToken().GetSecret()) + slog.Debug("received token create event", "tokenID", t.GetToken().GetId(), "tokenSecret", t.GetToken().GetSecret()) }) } } diff --git a/internal/parsing/code.go b/internal/parsing/code.go index 3d8bdec..8afab6c 100644 --- a/internal/parsing/code.go +++ b/internal/parsing/code.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" - "github.com/ExplorViz/trace-service/internal/attrib" semconv "go.opentelemetry.io/otel/semconv/v1.41.0" + + "github.com/ExplorViz/trace-service/internal/attrib" ) type CodeSpanEntity struct { diff --git a/main.go b/main.go index 6c86b69..1a4dec1 100644 --- a/main.go +++ b/main.go @@ -13,12 +13,12 @@ import ( "syscall" "time" - "github.com/ExplorViz/trace-service/internal/kafka/spanproc" - "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" "github.com/peterbourgon/ff/v4" "github.com/peterbourgon/ff/v4/ffhelp" - "github.com/twmb/franz-go/pkg/kgo" + + "github.com/ExplorViz/trace-service/internal/kafka/spanproc" + "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" ) func main() { From 96d156eb0a076ee620e97f2ccf4a28a57c242ec8 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:18:29 +0200 Subject: [PATCH 14/27] Add Dockerfile --- Dockerfile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..489a09f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.26 + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /trace-service + +EXPOSE 8081 + +CMD ["/trace-service"] \ No newline at end of file From a3bfc1b2fe8dd4126c7cbfe3c7e333c5a8cb854d Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:23:28 +0200 Subject: [PATCH 15/27] Fix GitLab CI incorrect format for artifacts path property --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6bf0dc2..299346a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,7 +47,8 @@ build: script: - gogo build -o trace-service . artifacts: - paths: ./trace-service + paths: + - ./trace-service code-lint: image: "golangci/golangci-lint:${GOLANGCI_LINT_VERSION}" From 1dbacb3e616b9d95de2e608dccf3003d8f9266ae Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:33:45 +0200 Subject: [PATCH 16/27] Fix typo in GitLab CI build script --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 299346a..2934cbe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,7 +45,7 @@ build: extends: .go-cache stage: build script: - - gogo build -o trace-service . + - go build -o trace-service . artifacts: paths: - ./trace-service From 8bd0fa7630ff1031ddcc0532ca71f1386d903572 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 10:43:27 +0200 Subject: [PATCH 17/27] Add missing stage declaration for proto-lint job, update .gitignore --- .gitignore | 4 ++-- .gitlab-ci.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 905d0c5..0589a43 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,8 @@ go.work.sum .env # Editor/IDE -# .idea/ -# .vscode/ +.idea/ +.vscode/ # Project-specific trace-service \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2934cbe..822bd81 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,7 @@ code-lint: proto-lint: image: "bufbuild/buf:${BUF_VERSION}" + stage: code-analysis rules: - changes: - "**/*.proto" From aa572996a7f0747496132e8d3feba5127ea1afb2 Mon Sep 17 00:00:00 2001 From: Malte Hansen Date: Thu, 18 Jun 2026 12:14:40 +0200 Subject: [PATCH 18/27] Update GitHub workflow --- .../workflows/{ci.yaml => deploy-docker.yaml} | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) rename .github/workflows/{ci.yaml => deploy-docker.yaml} (62%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/deploy-docker.yaml similarity index 62% rename from .github/workflows/ci.yaml rename to .github/workflows/deploy-docker.yaml index cbffff6..579a582 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/deploy-docker.yaml @@ -58,3 +58,28 @@ jobs: push: false archive: false pr_comment: false + + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [test, lint, buf] + + steps: + - uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to Docker Container Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build and push multi-arch Docker image + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ vars.DOCKER_IMAGE_NAME }}:latest . \ + --push \ No newline at end of file From eb844679c5aa96128358fdd2724ee5efb39f5f6e Mon Sep 17 00:00:00 2001 From: Malte Hansen Date: Thu, 18 Jun 2026 12:17:27 +0200 Subject: [PATCH 19/27] Add manual workflow dispatch --- .github/workflows/deploy-docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-docker.yaml b/.github/workflows/deploy-docker.yaml index 579a582..145de6e 100644 --- a/.github/workflows/deploy-docker.yaml +++ b/.github/workflows/deploy-docker.yaml @@ -7,6 +7,7 @@ on: pull_request: branches: - "**" + workflow_dispatch: env: GO_VERSION: "1.26.x" From 7aae8d761ce18ecb4bf06adaaab15921a02259ca Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 16:22:58 +0200 Subject: [PATCH 20/27] Use multi-stage build for Dockerfile --- Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 489a09f..46611e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM golang:1.26 +FROM golang:1.26 AS builder WORKDIR /app @@ -9,8 +9,12 @@ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o /trace-service +RUN CGO_ENABLED=0 GOOS=linux go build -o /trace-service -ldflags="-s -w" + +FROM scratch + +COPY --from=builder /trace-service /trace-service EXPOSE 8081 -CMD ["/trace-service"] \ No newline at end of file +ENTRYPOINT ["/trace-service"] From 60a2bfebd81e16744bfdcc2413186f486b1980c3 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Thu, 18 Jun 2026 21:15:58 +0200 Subject: [PATCH 21/27] Remember known span IDs and skip previously seen spans --- internal/attrib/spanreader.go | 26 ++++++-- internal/kafka/spanproc/spanproc.go | 87 ++++++++++++++++++++++----- internal/kafka/tokenproc/tokenproc.go | 5 +- internal/parsing/code.go | 10 +-- internal/parsing/common.go | 2 +- 5 files changed, 103 insertions(+), 27 deletions(-) diff --git a/internal/attrib/spanreader.go b/internal/attrib/spanreader.go index 5cde429..7aa05e0 100644 --- a/internal/attrib/spanreader.go +++ b/internal/attrib/spanreader.go @@ -29,16 +29,30 @@ func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resou } } -func (s SpanReader) SpanAttribute(key attribute.Key) *commonpb.AnyValue { - return s.spanAttributes[string(key)] +func (sr SpanReader) SpanAttribute(key attribute.Key) *commonpb.AnyValue { + return sr.spanAttributes[string(key)] } -func (s SpanReader) ScopeAttribute(key attribute.Key) *commonpb.AnyValue { - return s.scopeAttributes[string(key)] +func (sr SpanReader) ScopeAttribute(key attribute.Key) *commonpb.AnyValue { + return sr.scopeAttributes[string(key)] } -func (s SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { - return s.resourceAttributes[string(key)] +func (sr SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { + return sr.resourceAttributes[string(key)] +} + +func (sr SpanReader) TokenID() string { + if v := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue(); v != "" { + return v + } + return sr.SpanAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() +} + +func (sr SpanReader) TokenSecret() string { + if v := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue(); v != "" { + return v + } + return sr.SpanAttribute(ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() } func attrsToMap(attrs []*commonpb.KeyValue) map[string]*commonpb.AnyValue { diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go index c541413..0e14aab 100644 --- a/internal/kafka/spanproc/spanproc.go +++ b/internal/kafka/spanproc/spanproc.go @@ -22,15 +22,49 @@ import ( var lastReceivedSpans atomic.Uint64 var lastInvalidSpans atomic.Uint64 +var lastExportedSpans atomic.Uint64 +var lastKnownSpans atomic.Uint64 + +type spanCache struct { + mu sync.Mutex + m map[string]map[string]bool +} + +func newSpanCache() spanCache { + return spanCache{ + m: make(map[string]map[string]bool), + } +} + +func (s *spanCache) add(landscapeTokenID string, spanID string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.m[landscapeTokenID][spanID] = true +} + +func (s *spanCache) contains(landscapeTokenID string, spanID string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + m, ok := s.m[landscapeTokenID] + if !ok { + return false + } + + _, ok = m[spanID] + return ok +} func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc.TokenStore, logInterval time.Duration) { spans := make(chan *attrib.SpanReader) results := make(chan *spanpb.ParsedSpan) + knownSpans := newSpanCache() workerCount := runtime.NumCPU() var wg sync.WaitGroup for range workerCount { - wg.Go(func() { consumerWorker(ctx, spans, results, ts, validateTokens) }) + wg.Go(func() { consumerWorker(ctx, spans, results, &knownSpans, ts, validateTokens) }) } wg.Go(func() { producerWorker(ctx, results, cl) }) @@ -40,8 +74,12 @@ func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc defer ticker.Stop() for range ticker.C { - count := lastReceivedSpans.Swap(0) - slog.Info(fmt.Sprintf("received %d spans", count)) + slog.Info( + "span log interval", + "received", lastReceivedSpans.Swap(0), + "invalid", lastInvalidSpans.Swap(0), + "exported", lastExportedSpans.Swap(0), + "known", lastKnownSpans.Swap(0)) } }() } @@ -73,31 +111,34 @@ func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc wg.Wait() } -func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, results chan<- *spanpb.ParsedSpan, ts *tokenproc.TokenStore, validateTokens bool) { +func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, results chan<- *spanpb.ParsedSpan, sc *spanCache, ts *tokenproc.TokenStore, validateTokens bool) { for { select { case <-ctx.Done(): slog.Debug("exiting span consumer worker") return - case s := <-spans: + case sr := <-spans: lastReceivedSpans.Add(1) - if validateTokens { - tokenID := s.SpanAttribute(attrib.ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue() - tokenSecret := s.SpanAttribute(attrib.ExplorVizAttributes.LandscapeTokenSecret.Key).GetStringValue() - if !ts.HasToken(tokenID, tokenSecret) { - slog.Debug("invalid span: unknown landscape token ID or incorrect secret", "landscapeTokenID", tokenID) - lastInvalidSpans.Add(1) - continue - } + if err := validate(sr, ts, validateTokens); err != nil { + lastInvalidSpans.Add(1) + slog.Debug("received invalid span", "error", err) + } + + tokenID := sr.TokenID() + if sc.contains(tokenID, string(sr.Span.SpanId)) { + lastKnownSpans.Add(1) + slog.Debug("received already known span", "spanID", sr.Span.SpanId) + continue } - p, err := conversion.ConvertSpan(s) + p, err := conversion.ConvertSpan(sr) if err != nil { slog.Error("failed to convert span", "error", err) continue } + sc.add(tokenID, string(sr.Span.SpanId)) results <- conversion.ToProto(p) } } @@ -120,6 +161,24 @@ func producerWorker(ctx context.Context, results <-chan *spanpb.ParsedSpan, cl * Key: []byte(s.GetLandscapeTokenId()), Value: out, }) + lastExportedSpans.Add(1) + } + } +} + +func validate(sr *attrib.SpanReader, ts *tokenproc.TokenStore, validateTokens bool) error { + id := sr.TokenID() + secret := sr.TokenSecret() + + if id == "" || secret == "" { + return fmt.Errorf("missing landscape token ID or secret attribute") + } + + if validateTokens { + if !ts.HasToken(id, secret) { + return fmt.Errorf("unknown landscape token ID \"%s\" or incorrect secret", id) } } + + return nil } diff --git a/internal/kafka/tokenproc/tokenproc.go b/internal/kafka/tokenproc/tokenproc.go index a506ed6..c26c86a 100644 --- a/internal/kafka/tokenproc/tokenproc.go +++ b/internal/kafka/tokenproc/tokenproc.go @@ -16,7 +16,7 @@ import ( // A TokenStore keeps track of existing landscape tokens and their corresponding token secrets. type TokenStore struct { - mu sync.Mutex + mu sync.RWMutex m map[string]string } @@ -43,6 +43,9 @@ func (s *TokenStore) delete(id string) { // HasToken reports whether the store knows about a landscape token // with the given ID and secret func (s *TokenStore) HasToken(id string, secret string) bool { + s.mu.Lock() + defer s.mu.Unlock() + val, ok := s.m[id] return ok && val == secret } diff --git a/internal/parsing/code.go b/internal/parsing/code.go index 8afab6c..aaf868b 100644 --- a/internal/parsing/code.go +++ b/internal/parsing/code.go @@ -22,9 +22,9 @@ func (c CodeSpanEntity) Id() string { return c.FilePath + " " + c.FuncName + " " + c.ClassName + " " + c.GitCommitHash } -func ParseCodeSpan(s *attrib.SpanReader) (SpanEntity, error) { - fqn := s.SpanAttribute(semconv.CodeFunctionNameKey).GetStringValue() - lang := s.ResourceAttribute(semconv.TelemetrySDKLanguageKey).GetStringValue() +func ParseCodeSpan(sr *attrib.SpanReader) (SpanEntity, error) { + fqn := sr.SpanAttribute(semconv.CodeFunctionNameKey).GetStringValue() + lang := sr.ResourceAttribute(semconv.TelemetrySDKLanguageKey).GetStringValue() parsedFqn := ParseFunctionFqn(fqn, lang) @@ -32,13 +32,13 @@ func ParseCodeSpan(s *attrib.SpanReader) (SpanEntity, error) { return &CodeSpanEntity{}, errors.New("code parser: function name could not be extracted") } - filePath := cmp.Or(parsedFqn.FilePath, s.SpanAttribute(semconv.CodeFilePathKey).GetStringValue()) + filePath := cmp.Or(parsedFqn.FilePath, sr.SpanAttribute(semconv.CodeFilePathKey).GetStringValue()) if filePath == "" { return CodeSpanEntity{}, fmt.Errorf("code parser: file path could not be extracted from FQN and %s not given", semconv.CodeFilePathKey) } - gitHash := s.SpanAttribute(semconv.VCSRefHeadRevisionKey).GetStringValue() + gitHash := sr.SpanAttribute(semconv.VCSRefHeadRevisionKey).GetStringValue() return CodeSpanEntity{ FilePath: filePath, diff --git a/internal/parsing/common.go b/internal/parsing/common.go index 720273a..c584d6c 100644 --- a/internal/parsing/common.go +++ b/internal/parsing/common.go @@ -2,7 +2,7 @@ package parsing import "github.com/ExplorViz/trace-service/internal/attrib" -type SpanParser func(s *attrib.SpanReader) (SpanEntity, error) +type SpanParser func(sr *attrib.SpanReader) (SpanEntity, error) type SpanEntity interface { Id() string From b4548646250f3aa3d9a5087e0deb647f2d4f7e10 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Fri, 19 Jun 2026 11:30:15 +0200 Subject: [PATCH 22/27] Refactor token validation to use dependency injection --- internal/kafka/spanproc/spanproc.go | 29 +++++------ internal/kafka/tokenproc/tokenproc.go | 47 ++---------------- internal/token/validate.go | 69 +++++++++++++++++++++++++++ main.go | 11 +++-- 4 files changed, 95 insertions(+), 61 deletions(-) create mode 100644 internal/token/validate.go diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go index 0e14aab..d4510fa 100644 --- a/internal/kafka/spanproc/spanproc.go +++ b/internal/kafka/spanproc/spanproc.go @@ -15,7 +15,7 @@ import ( "github.com/ExplorViz/trace-service/internal/attrib" "github.com/ExplorViz/trace-service/internal/conversion" "github.com/ExplorViz/trace-service/internal/genproto/spanpb" - "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" + "github.com/ExplorViz/trace-service/internal/token" coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" ) @@ -56,7 +56,7 @@ func (s *spanCache) contains(landscapeTokenID string, spanID string) bool { return ok } -func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc.TokenStore, logInterval time.Duration) { +func Run(ctx context.Context, cl *kgo.Client, tv token.TokenValidator, logInterval time.Duration) { spans := make(chan *attrib.SpanReader) results := make(chan *spanpb.ParsedSpan) knownSpans := newSpanCache() @@ -64,7 +64,7 @@ func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc workerCount := runtime.NumCPU() var wg sync.WaitGroup for range workerCount { - wg.Go(func() { consumerWorker(ctx, spans, results, &knownSpans, ts, validateTokens) }) + wg.Go(func() { consumerWorker(ctx, spans, results, &knownSpans, tv) }) } wg.Go(func() { producerWorker(ctx, results, cl) }) @@ -111,7 +111,7 @@ func Run(ctx context.Context, cl *kgo.Client, validateTokens bool, ts *tokenproc wg.Wait() } -func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, results chan<- *spanpb.ParsedSpan, sc *spanCache, ts *tokenproc.TokenStore, validateTokens bool) { +func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, results chan<- *spanpb.ParsedSpan, sc *spanCache, tv token.TokenValidator) { for { select { case <-ctx.Done(): @@ -121,15 +121,15 @@ func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, result case sr := <-spans: lastReceivedSpans.Add(1) - if err := validate(sr, ts, validateTokens); err != nil { + if err := validate(sr, tv); err != nil { lastInvalidSpans.Add(1) slog.Debug("received invalid span", "error", err) } tokenID := sr.TokenID() - if sc.contains(tokenID, string(sr.Span.SpanId)) { + if sc.contains(tokenID, string(sr.Span.GetSpanId())) { lastKnownSpans.Add(1) - slog.Debug("received already known span", "spanID", sr.Span.SpanId) + slog.Debug("received already known span", "spanID", sr.Span.GetSpanId()) continue } @@ -138,7 +138,7 @@ func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, result slog.Error("failed to convert span", "error", err) continue } - sc.add(tokenID, string(sr.Span.SpanId)) + sc.add(tokenID, string(sr.Span.GetSpanId())) results <- conversion.ToProto(p) } } @@ -166,18 +166,15 @@ func producerWorker(ctx context.Context, results <-chan *spanpb.ParsedSpan, cl * } } -func validate(sr *attrib.SpanReader, ts *tokenproc.TokenStore, validateTokens bool) error { - id := sr.TokenID() - secret := sr.TokenSecret() +func validate(sr *attrib.SpanReader, tv token.TokenValidator) error { + t := token.LandscapeToken{ID: sr.TokenID(), Secret: sr.TokenSecret()} - if id == "" || secret == "" { + if t.ID == "" || t.Secret == "" { return fmt.Errorf("missing landscape token ID or secret attribute") } - if validateTokens { - if !ts.HasToken(id, secret) { - return fmt.Errorf("unknown landscape token ID \"%s\" or incorrect secret", id) - } + if err := tv.Validate(t); err != nil { + return fmt.Errorf("invalid token: %v", err) } return nil diff --git a/internal/kafka/tokenproc/tokenproc.go b/internal/kafka/tokenproc/tokenproc.go index c26c86a..72c12ea 100644 --- a/internal/kafka/tokenproc/tokenproc.go +++ b/internal/kafka/tokenproc/tokenproc.go @@ -1,58 +1,21 @@ // Package tokenproc is concerned with processing landscape token events, // such as the creation of new tokens and the deletion of existing tokens. -// It provides means of keeping track of existing landscape tokens and react package tokenproc import ( "context" "log/slog" - "sync" "github.com/twmb/franz-go/pkg/kgo" "google.golang.org/protobuf/proto" "github.com/ExplorViz/trace-service/internal/genproto/tokenpb" + "github.com/ExplorViz/trace-service/internal/token" ) -// A TokenStore keeps track of existing landscape tokens and their corresponding token secrets. -type TokenStore struct { - mu sync.RWMutex - m map[string]string -} - -func NewTokenStore() TokenStore { - return TokenStore{ - m: make(map[string]string), - } -} - -func (s *TokenStore) put(id string, secret string) { - s.mu.Lock() - defer s.mu.Unlock() - - s.m[id] = secret -} - -func (s *TokenStore) delete(id string) { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.m, id) -} - -// HasToken reports whether the store knows about a landscape token -// with the given ID and secret -func (s *TokenStore) HasToken(id string, secret string) bool { - s.mu.Lock() - defer s.mu.Unlock() - - val, ok := s.m[id] - return ok && val == secret -} - // Run continuously fetches records from the given client, attempts to deserialize them -// as [tokenpb.TokenEvent]s and updates the provided [TokenStore] accordingly. -func Run(ctx context.Context, cl *kgo.Client, ts *TokenStore) { +// as [tokenpb.TokenEvent]s and updates the provided [InMemTokenStore] accordingly. +func Run(ctx context.Context, cl *kgo.Client, ts *token.InMemTokenStore) { for { fs := cl.PollFetches(ctx) if ctx.Err() != nil { @@ -61,7 +24,7 @@ func Run(ctx context.Context, cl *kgo.Client, ts *TokenStore) { } fs.EachRecord(func(r *kgo.Record) { if r.Value == nil { - ts.delete(string(r.Key)) + ts.Delete(string(r.Key)) slog.Debug("received token delete event", "tokenID", r.Key) return } @@ -72,7 +35,7 @@ func Run(ctx context.Context, cl *kgo.Client, ts *TokenStore) { return } - ts.put(string(r.Key), t.GetToken().GetSecret()) + ts.Put(token.LandscapeToken{ID: string(r.Key), Secret: t.GetToken().GetSecret()}) slog.Debug("received token create event", "tokenID", t.GetToken().GetId(), "tokenSecret", t.GetToken().GetSecret()) }) } diff --git a/internal/token/validate.go b/internal/token/validate.go new file mode 100644 index 0000000..685c37e --- /dev/null +++ b/internal/token/validate.go @@ -0,0 +1,69 @@ +// Package token provides utilities for working with landscape tokens. +package token + +import ( + "fmt" + "sync" +) + +type LandscapeToken struct { + ID string + Secret string +} + +// A TokenValidator validates a landscape token, identified by its ID and secret. +type TokenValidator interface { + // Validate checks whether the given landscape token meets the requirements of this validator + // and returns an error if this is not the case. + Validate(token LandscapeToken) error +} + +// An InMemTokenStore uses a map to keep track of landscape tokens in-memory. +// Tokens can be manually added and removed from the store. +// The store is safe to read and write from multiple goroutines. +type InMemTokenStore struct { + mu sync.RWMutex + m map[string]string +} + +func NewInMemTokenStore() *InMemTokenStore { + return &InMemTokenStore{ + m: make(map[string]string), + } +} + +func (s *InMemTokenStore) Validate(t LandscapeToken) error { + s.mu.Lock() + defer s.mu.Unlock() + + val, ok := s.m[t.ID] + if ok && val == t.Secret { + return nil + } + return fmt.Errorf("unknown landscape token or incorrect secret") +} + +func (s *InMemTokenStore) Put(t LandscapeToken) { + s.mu.Lock() + defer s.mu.Unlock() + + s.m[t.ID] = t.Secret +} + +func (s *InMemTokenStore) Delete(tokenID string) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.m, tokenID) +} + +var _ TokenValidator = (*InMemTokenStore)(nil) + +// A NoOpTokenValidator stores no data and treats all landscape token as valid. +type NoOpTokenValidator struct{} + +func (s NoOpTokenValidator) Validate(t LandscapeToken) error { + return nil +} + +var _ TokenValidator = NoOpTokenValidator{} diff --git a/main.go b/main.go index 1a4dec1..89354f3 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/ExplorViz/trace-service/internal/kafka/spanproc" "github.com/ExplorViz/trace-service/internal/kafka/tokenproc" + "github.com/ExplorViz/trace-service/internal/token" ) func main() { @@ -88,12 +89,16 @@ func main() { }() var wg sync.WaitGroup - ts := tokenproc.NewTokenStore() - wg.Go(func() { spanproc.Run(ctx, spanCl, *validateTokens, &ts, *logInterval) }) + var tv token.TokenValidator if *validateTokens { - wg.Go(func() { tokenproc.Run(ctx, tokenCl, &ts) }) + inmem := token.NewInMemTokenStore() + wg.Go(func() { tokenproc.Run(ctx, tokenCl, inmem) }) + tv = inmem + } else { + tv = token.NoOpTokenValidator{} } + wg.Go(func() { spanproc.Run(ctx, spanCl, tv, *logInterval) }) fmt.Print(` ______ _ __ ___ From 7788c752634d870958037cfc002d9817ccdd6f5a Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sat, 20 Jun 2026 10:54:07 +0200 Subject: [PATCH 23/27] Add Git pre-commit hook --- githooks/pre-commit | 39 ++++++++++++++++ internal/kafka/spanproc/spanproc.go | 10 ++--- main.go | 5 ++- scripts/{ => genproto}/genproto.go | 6 ++- scripts/reghook/reghook.go | 69 +++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 8 deletions(-) create mode 100755 githooks/pre-commit rename scripts/{ => genproto}/genproto.go (88%) create mode 100644 scripts/reghook/reghook.go diff --git a/githooks/pre-commit b/githooks/pre-commit new file mode 100755 index 0000000..d4a0b3d --- /dev/null +++ b/githooks/pre-commit @@ -0,0 +1,39 @@ +#!/bin/bash + +cleanup() { + local result=$? + + if [ $result -eq 1 ] + then + echo "Pre-commit hook failed." + else + echo "Pre-commit hook passed." + fi + + exit $result +} + +trap cleanup EXIT + +echo "Running pre-commit hook" +echo +echo "NOTE: The pre-commit hook runs on all files in the working tree, +not just on staged changes. If this leads to false positives, you can +stash your unstaged changes before committing again." +echo + +if ! command -v go &> /dev/null; then + echo "Command \"go\" is not available." + echo "Make sure to install it or bypass the hook via the --no-verify flag" + exit 1 +fi + + +if ! command -v golangci-lint &> /dev/null; then + echo "Command \"golangci-lint\" is not available." + echo "Make sure to install it or bypass the hook via the --no-verify flag" + exit 1 +fi + +golangci-lint run || { echo "There are linting issues present."; exit 1; } +go test ./... || { echo "There are failing tests."; exit 1; } \ No newline at end of file diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go index d4510fa..047836d 100644 --- a/internal/kafka/spanproc/spanproc.go +++ b/internal/kafka/spanproc/spanproc.go @@ -23,7 +23,7 @@ import ( var lastReceivedSpans atomic.Uint64 var lastInvalidSpans atomic.Uint64 var lastExportedSpans atomic.Uint64 -var lastKnownSpans atomic.Uint64 +var lastDuplicateSpans atomic.Uint64 type spanCache struct { mu sync.Mutex @@ -75,11 +75,11 @@ func Run(ctx context.Context, cl *kgo.Client, tv token.TokenValidator, logInterv for range ticker.C { slog.Info( - "span log interval", + "logging span counts since last log interval", "received", lastReceivedSpans.Swap(0), "invalid", lastInvalidSpans.Swap(0), "exported", lastExportedSpans.Swap(0), - "known", lastKnownSpans.Swap(0)) + "duplicates", lastDuplicateSpans.Swap(0)) } }() } @@ -128,8 +128,8 @@ func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, result tokenID := sr.TokenID() if sc.contains(tokenID, string(sr.Span.GetSpanId())) { - lastKnownSpans.Add(1) - slog.Debug("received already known span", "spanID", sr.Span.GetSpanId()) + lastDuplicateSpans.Add(1) + slog.Debug("received already seen span ID", "spanID", sr.Span.GetSpanId()) continue } diff --git a/main.go b/main.go index 89354f3..607108e 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,5 @@ -//go:generate go run ./scripts/genproto.go +//go:generate go run ./scripts/genproto/genproto.go +//go:generate go run ./scripts/reghook/reghook.go package main @@ -29,7 +30,7 @@ func main() { inTopic = fs.String('i', "topic-in", "telemetry.spans.raw", "Kafka topic to consume OpenTelemetry OTLP spans from") outTopic = fs.String('o', "topic-out", "telemetry.spans.parsed", "Kafka topic to produce parsed spans into") tokensTopic = fs.String('t', "topic-tokens", "tokens.events", "Kafka topic to consume landscape token events from") - validateTokens = fs.BoolLong("validate-tokens", "whether to verify the existence of provided landscape tokens for incoming traces") + validateTokens = fs.Bool('v', "validate-tokens", "whether to verify the existence of provided landscape tokens for incoming traces") logLevel = fs.StringEnum('l', "log-level", "log level: info, error, debug", "info", "error", "debug") logInterval = fs.DurationLong("log-interval", 5*time.Second, "interval at which received spans should be logged (0 to disable)") ) diff --git a/scripts/genproto.go b/scripts/genproto/genproto.go similarity index 88% rename from scripts/genproto.go rename to scripts/genproto/genproto.go index 8063cba..ab34aca 100644 --- a/scripts/genproto.go +++ b/scripts/genproto/genproto.go @@ -16,6 +16,8 @@ import ( ) func main() { + fmt.Println("generating protobuf code") + files, err := filepath.Glob("./proto/*.proto") if err != nil { fmt.Printf("error matching glob pattern: %v\n", err) @@ -23,7 +25,7 @@ func main() { } if len(files) == 0 { - fmt.Println("no matching files found") + fmt.Println("no matching proto files found") return } @@ -37,4 +39,6 @@ func main() { fmt.Printf("protobuf generation failed: %v\n", err) os.Exit(1) } + + fmt.Println("successfully generated protobuf code") } diff --git a/scripts/reghook/reghook.go b/scripts/reghook/reghook.go new file mode 100644 index 0000000..6c9ff46 --- /dev/null +++ b/scripts/reghook/reghook.go @@ -0,0 +1,69 @@ +//go:build ignore + +// Command reghook registers the hooks provided within the githooks folder of this repository. +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func main() { + sourceDir := "githooks" + targetDir := filepath.Join(".git", "hooks") + + if err := os.MkdirAll(targetDir, 0755); err != nil { + fmt.Printf("failed to create hooks directory: %v\n", err) + os.Exit(1) + } + + entries, err := os.ReadDir(sourceDir) + if err != nil { + fmt.Printf("failed to read githooks directory: %v\n", err) + os.Exit(1) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + srcPath := filepath.Join(sourceDir, entry.Name()) + dstPath := filepath.Join(targetDir, entry.Name()) + + if err := copyFile(srcPath, dstPath); err != nil { + fmt.Printf("failed to copy %s: %v\n", entry.Name(), err) + continue + } + + if err := os.Chmod(dstPath, 0755); err != nil { + fmt.Printf("failed to set executable for %s: %v\n", entry.Name(), err) + } + + fmt.Println("installed hook:", entry.Name()) + } + + fmt.Println("successfully installed Git pre-commit hooks") +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return out.Sync() +} From 62f621120a80b94ad8d0449c18586ee4dcf4b05e Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sat, 20 Jun 2026 12:34:31 +0200 Subject: [PATCH 24/27] Fixes: Missing map ok check, SpanReader constructor missing assignments, read OTel IDs as hexadecimal --- internal/attrib/spanreader.go | 18 ++++++++++++++++++ internal/conversion/span.go | 6 +++--- internal/kafka/spanproc/spanproc.go | 14 ++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/internal/attrib/spanreader.go b/internal/attrib/spanreader.go index 7aa05e0..b9de6d9 100644 --- a/internal/attrib/spanreader.go +++ b/internal/attrib/spanreader.go @@ -1,6 +1,8 @@ package attrib import ( + "encoding/hex" + "go.opentelemetry.io/otel/attribute" commonpb "go.opentelemetry.io/proto/otlp/common/v1" @@ -23,6 +25,10 @@ type SpanReader struct { func NewSpanReader(s *tracepb.Span, sc *commonpb.InstrumentationScope, rs *resourcepb.Resource) SpanReader { return SpanReader{ + Span: s, + Scope: sc, + Resource: rs, + spanAttributes: attrsToMap(s.GetAttributes()), scopeAttributes: attrsToMap(sc.GetAttributes()), resourceAttributes: attrsToMap(rs.GetAttributes()), @@ -41,6 +47,18 @@ func (sr SpanReader) ResourceAttribute(key attribute.Key) *commonpb.AnyValue { return sr.resourceAttributes[string(key)] } +func (sr SpanReader) TraceID() string { + return hex.EncodeToString(sr.Span.GetTraceId()) +} + +func (sr SpanReader) SpanID() string { + return hex.EncodeToString(sr.Span.GetSpanId()) +} + +func (sr SpanReader) ParentSpanID() string { + return hex.EncodeToString(sr.Span.GetParentSpanId()) +} + func (sr SpanReader) TokenID() string { if v := sr.ResourceAttribute(ExplorVizAttributes.LandscapeTokenID.Key).GetStringValue(); v != "" { return v diff --git a/internal/conversion/span.go b/internal/conversion/span.go index 9936cff..49dd612 100644 --- a/internal/conversion/span.go +++ b/internal/conversion/span.go @@ -45,9 +45,9 @@ func ConvertSpan(sr *attrib.SpanReader) (ParsedSpan, error) { LandscapeTokenId: tokenId, LandscapeTokenSecret: tokenSecret, - TraceId: string(sr.Span.GetTraceId()), - SpanId: string(sr.Span.GetSpanId()), - ParentSpanId: string(sr.Span.GetParentSpanId()), + TraceId: sr.TraceID(), + SpanId: sr.SpanID(), + ParentSpanId: sr.ParentSpanID(), StartTime: sr.Span.GetStartTimeUnixNano(), EndTime: sr.Span.GetEndTimeUnixNano(), diff --git a/internal/kafka/spanproc/spanproc.go b/internal/kafka/spanproc/spanproc.go index 047836d..c687168 100644 --- a/internal/kafka/spanproc/spanproc.go +++ b/internal/kafka/spanproc/spanproc.go @@ -40,7 +40,13 @@ func (s *spanCache) add(landscapeTokenID string, spanID string) { s.mu.Lock() defer s.mu.Unlock() - s.m[landscapeTokenID][spanID] = true + m, ok := s.m[landscapeTokenID] + if !ok { + s.m[landscapeTokenID] = make(map[string]bool) + m = s.m[landscapeTokenID] + } + + m[spanID] = true } func (s *spanCache) contains(landscapeTokenID string, spanID string) bool { @@ -127,9 +133,9 @@ func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, result } tokenID := sr.TokenID() - if sc.contains(tokenID, string(sr.Span.GetSpanId())) { + if sc.contains(tokenID, sr.SpanID()) { lastDuplicateSpans.Add(1) - slog.Debug("received already seen span ID", "spanID", sr.Span.GetSpanId()) + slog.Debug("received already seen span ID", "spanID", sr.SpanID()) continue } @@ -138,7 +144,7 @@ func consumerWorker(ctx context.Context, spans <-chan *attrib.SpanReader, result slog.Error("failed to convert span", "error", err) continue } - sc.add(tokenID, string(sr.Span.GetSpanId())) + sc.add(tokenID, sr.SpanID()) results <- conversion.ToProto(p) } } From 684356fbe2f0b36041b8b279303b2e5074505143 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sun, 21 Jun 2026 12:23:23 +0200 Subject: [PATCH 25/27] Move protobuf entity_id from entity_descriptor to ParsedSpan message --- internal/conversion/protobuf.go | 2 + internal/conversion/span.go | 4 +- internal/genproto/spanpb/parsed_span.pb.go | 52 +++++++++++----------- internal/parsing/fqn.go | 2 +- proto/parsed_span.proto | 15 ++++--- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/internal/conversion/protobuf.go b/internal/conversion/protobuf.go index e33756b..efd67c8 100644 --- a/internal/conversion/protobuf.go +++ b/internal/conversion/protobuf.go @@ -19,6 +19,8 @@ func ToProto(ps ParsedSpan) *spanpb.ParsedSpan { EndTime: ps.EndTime, ApplicationName: ps.ApplicationName, + + EntityId: ps.Entity.Id(), } switch e := ps.Entity.(type) { diff --git a/internal/conversion/span.go b/internal/conversion/span.go index 49dd612..a165648 100644 --- a/internal/conversion/span.go +++ b/internal/conversion/span.go @@ -36,7 +36,7 @@ func ConvertSpan(sr *attrib.SpanReader) (ParsedSpan, error) { appName := sr.ResourceAttribute(semconv.ServiceNameKey).GetStringValue() for _, parser := range parserChain { - res, err := parser(sr) + entity, err := parser(sr) if err != nil { slog.Debug("parser failed", "error", err) continue @@ -53,7 +53,7 @@ func ConvertSpan(sr *attrib.SpanReader) (ParsedSpan, error) { EndTime: sr.Span.GetEndTimeUnixNano(), ApplicationName: appName, - Entity: res, + Entity: entity, }, nil } return ParsedSpan{}, errors.New("no matching span parser found") diff --git a/internal/genproto/spanpb/parsed_span.pb.go b/internal/genproto/spanpb/parsed_span.pb.go index ddd29b4..5c8946b 100644 --- a/internal/genproto/spanpb/parsed_span.pb.go +++ b/internal/genproto/spanpb/parsed_span.pb.go @@ -32,6 +32,7 @@ type ParsedSpan struct { StartTime uint64 `protobuf:"varint,7,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,8,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` ApplicationName string `protobuf:"bytes,9,opt,name=application_name,json=applicationName,proto3" json:"application_name,omitempty"` + EntityId string `protobuf:"bytes,10,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` // Identifier for the entity described by the span (e.g. function, database, ...) // Types that are valid to be assigned to EntityDescriptor: // // *ParsedSpan_CodeDescriptor @@ -133,6 +134,13 @@ func (x *ParsedSpan) GetApplicationName() string { return "" } +func (x *ParsedSpan) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + func (x *ParsedSpan) GetEntityDescriptor() isParsedSpan_EntityDescriptor { if x != nil { return x.EntityDescriptor @@ -154,19 +162,18 @@ type isParsedSpan_EntityDescriptor interface { } type ParsedSpan_CodeDescriptor struct { - CodeDescriptor *CodeDescriptor `protobuf:"bytes,10,opt,name=code_descriptor,json=codeDescriptor,proto3,oneof"` + CodeDescriptor *CodeDescriptor `protobuf:"bytes,11,opt,name=code_descriptor,json=codeDescriptor,proto3,oneof"` } func (*ParsedSpan_CodeDescriptor) isParsedSpan_EntityDescriptor() {} type CodeDescriptor struct { state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` - FunctionName string `protobuf:"bytes,3,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` - ClassName *string `protobuf:"bytes,4,opt,name=class_name,json=className,proto3,oneof" json:"class_name,omitempty"` - Language *string `protobuf:"bytes,5,opt,name=language,proto3,oneof" json:"language,omitempty"` - GitCommitHash *string `protobuf:"bytes,6,opt,name=git_commit_hash,json=gitCommitHash,proto3,oneof" json:"git_commit_hash,omitempty"` + FilePath string `protobuf:"bytes,1,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + FunctionName string `protobuf:"bytes,2,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` + ClassName *string `protobuf:"bytes,3,opt,name=class_name,json=className,proto3,oneof" json:"class_name,omitempty"` // Inner classes may be separated by "." + Language *string `protobuf:"bytes,4,opt,name=language,proto3,oneof" json:"language,omitempty"` + GitCommitHash *string `protobuf:"bytes,5,opt,name=git_commit_hash,json=gitCommitHash,proto3,oneof" json:"git_commit_hash,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -201,13 +208,6 @@ func (*CodeDescriptor) Descriptor() ([]byte, []int) { return file_parsed_span_proto_rawDescGZIP(), []int{1} } -func (x *CodeDescriptor) GetId() string { - if x != nil { - return x.Id - } - return "" -} - func (x *CodeDescriptor) GetFilePath() string { if x != nil { return x.FilePath @@ -247,7 +247,7 @@ var File_parsed_span_proto protoreflect.FileDescriptor const file_parsed_span_proto_rawDesc = "" + "\n" + - "\x11parsed_span.proto\"\xa7\x03\n" + + "\x11parsed_span.proto\"\xc4\x03\n" + "\n" + "ParsedSpan\x12,\n" + "\x12landscape_token_id\x18\x01 \x01(\tR\x10landscapeTokenId\x124\n" + @@ -259,20 +259,20 @@ const file_parsed_span_proto_rawDesc = "" + "\n" + "start_time\x18\a \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\b \x01(\x04R\aendTime\x12)\n" + - "\x10application_name\x18\t \x01(\tR\x0fapplicationName\x12:\n" + - "\x0fcode_descriptor\x18\n" + - " \x01(\v2\x0f.CodeDescriptorH\x00R\x0ecodeDescriptorB\x13\n" + + "\x10application_name\x18\t \x01(\tR\x0fapplicationName\x12\x1b\n" + + "\tentity_id\x18\n" + + " \x01(\tR\bentityId\x12:\n" + + "\x0fcode_descriptor\x18\v \x01(\v2\x0f.CodeDescriptorH\x00R\x0ecodeDescriptorB\x13\n" + "\x11entity_descriptorB\f\n" + "\n" + - "_parent_id\"\x84\x02\n" + - "\x0eCodeDescriptor\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x1b\n" + - "\tfile_path\x18\x02 \x01(\tR\bfilePath\x12#\n" + - "\rfunction_name\x18\x03 \x01(\tR\ffunctionName\x12\"\n" + + "_parent_id\"\xf4\x01\n" + + "\x0eCodeDescriptor\x12\x1b\n" + + "\tfile_path\x18\x01 \x01(\tR\bfilePath\x12#\n" + + "\rfunction_name\x18\x02 \x01(\tR\ffunctionName\x12\"\n" + "\n" + - "class_name\x18\x04 \x01(\tH\x00R\tclassName\x88\x01\x01\x12\x1f\n" + - "\blanguage\x18\x05 \x01(\tH\x01R\blanguage\x88\x01\x01\x12+\n" + - "\x0fgit_commit_hash\x18\x06 \x01(\tH\x02R\rgitCommitHash\x88\x01\x01B\r\n" + + "class_name\x18\x03 \x01(\tH\x00R\tclassName\x88\x01\x01\x12\x1f\n" + + "\blanguage\x18\x04 \x01(\tH\x01R\blanguage\x88\x01\x01\x12+\n" + + "\x0fgit_commit_hash\x18\x05 \x01(\tH\x02R\rgitCommitHash\x88\x01\x01B\r\n" + "\v_class_nameB\v\n" + "\t_languageB\x12\n" + "\x10_git_commit_hashB=Z;github.com/ExplorViz/trace-service/internal/genproto/spanpbb\x06proto3" diff --git a/internal/parsing/fqn.go b/internal/parsing/fqn.go index 64f79a2..e2cc243 100644 --- a/internal/parsing/fqn.go +++ b/internal/parsing/fqn.go @@ -39,7 +39,7 @@ func parseJavaFqn(fqn string) FqnParseResult { // Ignore lambda portion. If we explicitly want to support this, the data model would need to allow functions // to have child functions. For now, we simplify and treat this as a call of the containing function. - fqnNoLambda := strings.SplitN(fqn, "$", 2)[0] + fqnNoLambda, _, _ := strings.Cut(fqn, "$") separatedFqn := strings.Split(fqnNoLambda, ".") lastPackageIndex := -1 diff --git a/proto/parsed_span.proto b/proto/parsed_span.proto index a3083da..907ff19 100644 --- a/proto/parsed_span.proto +++ b/proto/parsed_span.proto @@ -16,16 +16,17 @@ message ParsedSpan { string application_name = 9; + string entity_id = 10; // Identifier for the entity described by the span (e.g. function, database, ...) + oneof entity_descriptor { - CodeDescriptor code_descriptor = 10; + CodeDescriptor code_descriptor = 11; } } message CodeDescriptor { - string id = 1; - string file_path = 2; - string function_name = 3; - optional string class_name = 4; - optional string language = 5; - optional string git_commit_hash = 6; + string file_path = 1; + string function_name = 2; + optional string class_name = 3; // Inner classes may be separated by "." + optional string language = 4; + optional string git_commit_hash = 5; } \ No newline at end of file From 674acf7ef4062a7456cbad0773e8e12b400e51b3 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sun, 21 Jun 2026 17:40:36 +0200 Subject: [PATCH 26/27] Fix proto-lint job failing due to buf image entrypoint --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 822bd81..7a67ef7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,7 +61,9 @@ code-lint: codequality: gl-code-quality-report.json proto-lint: - image: "bufbuild/buf:${BUF_VERSION}" + image: + name: "bufbuild/buf:${BUF_VERSION}" + entrypoint: [""] stage: code-analysis rules: - changes: From 01072049256127e282ced3a6459cc74a83793bd7 Mon Sep 17 00:00:00 2001 From: Moritz Bugla Date: Sun, 21 Jun 2026 19:49:17 +0200 Subject: [PATCH 27/27] Fix missing space in buf.yaml, rename token EventType enum values --- buf.yaml | 2 +- internal/genproto/tokenpb/token_event.pb.go | 51 +++++++++++---------- internal/kafka/tokenproc/tokenproc.go | 14 ++++-- proto/token_event.proto | 11 +++-- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/buf.yaml b/buf.yaml index 98b5b0d..0d2aeba 100644 --- a/buf.yaml +++ b/buf.yaml @@ -1,4 +1,4 @@ # Buf is used for CI/CD linting of .proto files version: v2 modules: - -path: . \ No newline at end of file + - path: . diff --git a/internal/genproto/tokenpb/token_event.pb.go b/internal/genproto/tokenpb/token_event.pb.go index 8eef0fa..7fa8711 100644 --- a/internal/genproto/tokenpb/token_event.pb.go +++ b/internal/genproto/tokenpb/token_event.pb.go @@ -24,28 +24,31 @@ const ( type EventType int32 const ( - EventType_CREATED EventType = 0 - EventType_DELETED EventType = 1 - EventType_CLONED EventType = 2 - EventType_ACCESS_GRANTED EventType = 3 - EventType_ACCESS_REVOKED EventType = 4 + EventType_EVENT_TYPE_UNSPECIFIED EventType = 0 + EventType_EVENT_TYPE_CREATED EventType = 1 + EventType_EVENT_TYPE_DELETED EventType = 2 + EventType_EVENT_TYPE_CLONED EventType = 3 + EventType_EVENT_TYPE_ACCESS_GRANTED EventType = 4 + EventType_EVENT_TYPE_ACCESS_REVOKED EventType = 5 ) // Enum value maps for EventType. var ( EventType_name = map[int32]string{ - 0: "CREATED", - 1: "DELETED", - 2: "CLONED", - 3: "ACCESS_GRANTED", - 4: "ACCESS_REVOKED", + 0: "EVENT_TYPE_UNSPECIFIED", + 1: "EVENT_TYPE_CREATED", + 2: "EVENT_TYPE_DELETED", + 3: "EVENT_TYPE_CLONED", + 4: "EVENT_TYPE_ACCESS_GRANTED", + 5: "EVENT_TYPE_ACCESS_REVOKED", } EventType_value = map[string]int32{ - "CREATED": 0, - "DELETED": 1, - "CLONED": 2, - "ACCESS_GRANTED": 3, - "ACCESS_REVOKED": 4, + "EVENT_TYPE_UNSPECIFIED": 0, + "EVENT_TYPE_CREATED": 1, + "EVENT_TYPE_DELETED": 2, + "EVENT_TYPE_CLONED": 3, + "EVENT_TYPE_ACCESS_GRANTED": 4, + "EVENT_TYPE_ACCESS_REVOKED": 5, } ) @@ -119,7 +122,7 @@ func (x *TokenEvent) GetType() EventType { if x != nil { return x.Type } - return EventType_CREATED + return EventType_EVENT_TYPE_UNSPECIFIED } func (x *TokenEvent) GetToken() *LandscapeToken { @@ -229,14 +232,14 @@ const file_token_event_proto_rawDesc = "" + "\x06secret\x18\x02 \x01(\tR\x06secret\x12\x14\n" + "\x05alias\x18\x03 \x01(\tR\x05alias\x12\x19\n" + "\bowner_id\x18\x04 \x01(\tR\aownerId\x12\x18\n" + - "\acreated\x18\x05 \x01(\x04R\acreated*Y\n" + - "\tEventType\x12\v\n" + - "\aCREATED\x10\x00\x12\v\n" + - "\aDELETED\x10\x01\x12\n" + - "\n" + - "\x06CLONED\x10\x02\x12\x12\n" + - "\x0eACCESS_GRANTED\x10\x03\x12\x12\n" + - "\x0eACCESS_REVOKED\x10\x04B>ZZ