diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 8be0ac7a3..0ba30899e 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -5,9 +5,27 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: kubernetes-test: - runs-on: large-runner + runs-on: warp-ubuntu-latest-x64-8x-spot + strategy: + fail-fast: false + matrix: + kube-version: + - "1.23" + - "1.30" + test-scenario: + - "multi-apps" + - "helm-chart" + include: + - kube-version: "1.23" + kind-image: "kindest/node:v1.23.17@sha256:14d0a9a892b943866d7e6be119a06871291c517d279aedb816a4b4bc0ec0a5b3" + - kube-version: "1.30" + kind-image: "kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e" steps: - name: Checkout uses: actions/checkout@v4 @@ -18,15 +36,20 @@ jobs: with: go-version: "~1.22" check-latest: true + cache: true + cache-dependency-path: | + **/go.sum - name: Set up Helm uses: azure/setup-helm@v4 with: version: v3.9.0 - - name: Setup BATS - uses: mig4/setup-bats@v1 + - name: Install chainsaw + uses: kyverno/action-install-chainsaw@v0.2.4 - name: Create Kind Cluster uses: helm/kind-action@v1.10.0 with: + node_image: ${{ matrix.kind-image }} + version: "v0.23.0" cluster_name: kind - name: Build CLI run: | @@ -35,64 +58,6 @@ jobs: - name: Build and Load Odigos Images run: | TAG=e2e-test make build-images load-to-kind - - name: Install Odigos - run: | - cli/odigos install --version e2e-test - - name: Install Collector - Add Dependencies - shell: bash - run: | - helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts - - uses: actions/checkout@v4 - with: - repository: 'open-telemetry/opentelemetry-helm-charts' - path: opentelemetry-helm-charts - - name: Install Collector - Helm install - run: helm install test -f .github/workflows/e2e/collector-helm-values.yaml opentelemetry-helm-charts/charts/opentelemetry-collector --namespace traces --create-namespace - - name: Wait for Collector to be ready - run: | - kubectl wait --for=condition=Ready --timeout=60s -n traces pod/test-opentelemetry-collector-0 - - name: Install KV Shop - run: | - kubectl create ns kvshop - kubectl apply -f .github/workflows/e2e/kv-shop.yaml -n kvshop - - name: Wait for KV Shop to be ready - run: | - kubectl wait --for=condition=Ready --timeout=100s -n kvshop pods --all - - name: Select kvshop namespace for instrumentation - run: | - kubectl label namespace kvshop odigos-instrumentation=enabled - - name: Connect to Jaeger destination - run: | - kubectl create -f .github/workflows/e2e/jaeger-dest.yaml - - name: Wait for Odigos to bring up collectors - run: | - while [[ $(kubectl get daemonset odigos-data-collection -n odigos-system -o jsonpath='{.status.numberReady}') != 1 ]]; - do - echo "Waiting for odigos-data-collection daemonset to be created" && sleep 3; - done - while [[ $(kubectl get deployment odigos-gateway -n odigos-system -o jsonpath='{.status.readyReplicas}') != 1 ]]; - do - echo "Waiting for odigos-data-collection deployment to be created" && sleep 3; - done - while [[ $(kubectl get pods --output=jsonpath='{range .items[*]}{.status.phase}{"\n"}{end}' -n kvshop | grep -v Running | wc -l) != 0 ]]; - do - echo "Waiting for kvshop pods to be running" && sleep 3; - done - sleep 10 - kubectl get pods -A - kubectl get svc -A - - name: Start bot job - run: | - kubectl create -f .github/workflows/e2e/buybot-job.yaml -n kvshop - - name: Wait for bot job to complete - run: | - kubectl wait --for=condition=Complete --timeout=60s job/buybot-job -n kvshop - - name: Copy trace output - run: | - echo "Sleeping for 10 seconds to allow traces to be collected" - sleep 10 - kubectl cp -c filecp traces/test-opentelemetry-collector-0:tmp/trace.json ./.github/workflows/e2e/bats/traces-orig.json - cat ./.github/workflows/e2e/bats/traces-orig.json - - name: Verify output trace + - name: Run E2E Tests run: | - bats .github/workflows/e2e/bats/verify.bats + chainsaw test tests/e2e/${{ matrix.test-scenario }} diff --git a/.github/workflows/e2e/bats/utilities.bash b/.github/workflows/e2e/bats/utilities.bash deleted file mode 100644 index c53a9ca39..000000000 --- a/.github/workflows/e2e/bats/utilities.bash +++ /dev/null @@ -1,166 +0,0 @@ -# DATA RETRIEVERS - -# Returns a list of span names emitted by a given library/scope - # $1 - library/scope name -span_names_for() { - spans_from_scope_named $1 | jq '.name' -} - -# Returns a list of server span names emitted by a given library/scope - # $1 - library/scope name -server_span_names_for() { - server_spans_from_scope_named $1 | jq '.name' -} - -# Returns a list of client span names emitted by a given library/scope - # $1 - library/scope name -client_span_names_for() { - client_spans_from_scope_named $1 | jq '.name' -} - -# Returns a list of attributes emitted by a given library/scope -span_attributes_for() { - # $1 - library/scope name - - spans_from_scope_named $1 | \ - jq ".attributes[]" -} - -# Returns a list of attributes emitted by a given library/scope on server spans. -server_span_attributes_for() { - # $1 - library/scope name - - server_spans_from_scope_named $1 | \ - jq ".attributes[]" -} - -# Returns a list of attributes emitted by a given library/scope on clinet_spans. -client_span_attributes_for() { - # $1 - library/scope name - - client_spans_from_scope_named $1 | \ - jq ".attributes[]" -} - -# Returns a list of all resource attributes -resource_attributes_received() { - spans_received | jq ".resource.attributes[]?" -} - -# Returns an array of all spans emitted by a given library/scope - # $1 - library/scope name -spans_from_scope_named() { - spans_received | jq ".scopeSpans[] | select(.scope.name == \"$1\").spans[]" -} - -# Returns an array of all server spans emitted by a given library/scope - # $1 - library/scope name -server_spans_from_scope_named() { - spans_from_scope_named $1 | jq "select(.kind == 2)" -} - -# Returns an array of all client spans emitted by a given library/scope - # $1 - library/scope name -client_spans_from_scope_named() { - spans_from_scope_named $1 | jq "select(.kind == 3)" -} - -# Returns an array of all spans received -spans_received() { - json_output | jq ".resourceSpans[]?" -} - -# Returns the content of the log file produced by a collector -# and located in the same directory as the BATS test file -# loading this helper script. -json_output() { - cat "${BATS_TEST_DIRNAME}/traces-orig.json" -} - -redact_json() { - json_output | \ - jq --sort-keys ' - del( - .resourceSpans[].scopeSpans[].spans[].startTimeUnixNano, - .resourceSpans[].scopeSpans[].spans[].endTimeUnixNano - ) - | .resourceSpans[].scopeSpans[].spans[].traceId|= (if - . // "" | test("^[A-Fa-f0-9]{32}$") then "xxxxx" else (. + "<-INVALID") - end) - | .resourceSpans[].scopeSpans[].spans[].spanId|= (if - . // "" | test("^[A-Fa-f0-9]{16}$") then "xxxxx" else (. + "<-INVALID") - end) - | .resourceSpans[].scopeSpans[].spans[].parentSpanId|= (if - . // "" | test("^[A-Fa-f0-9]{16}$") then "xxxxx" else (. + "") - end) - | .resourceSpans[].scopeSpans|=sort_by(.scope.name) - | .resourceSpans[].scopeSpans[].spans|=sort_by(.kind) - ' > ${BATS_TEST_DIRNAME}/traces.json -} - -# ASSERTION HELPERS - -# expect a 32-digit hexadecimal string (in quotes) -MATCH_A_TRACE_ID=^"\"[A-Fa-f0-9]{32}\"$" - -# expect a 16-digit hexadecimal string (in quotes) -MATCH_A_SPAN_ID=^"\"[A-Fa-f0-9]{16}\"$" - -# Fail and display details if the expected and actual values do not -# equal. Details include both values. -# -# Inspired by bats-assert * bats-support, but dramatically simplified -assert_equal() { - if [[ $1 != "$2" ]]; then - { - echo - echo "-- πŸ’₯ values are not equal πŸ’₯ --" - echo "expected : $2" - echo "actual : $1" - echo "--" - echo - } >&2 # output error to STDERR - return 1 - fi -} - -assert_ge() { - if [[ $1 -lt $2 ]]; then - { - echo - echo "-- πŸ’₯ Assertion failed: value is not greater than or equal to expected πŸ’₯ --" - echo "expected to be greater than or equal to: $2" - echo "actual: $1" - echo "--" - echo - } >&2 # output error to STDERR - return 1 - fi -} - -assert_regex() { - if ! [[ $1 =~ $2 ]]; then - { - echo - echo "-- πŸ’₯ value does not match regular expression πŸ’₯ --" - echo "value : $1" - echo "pattern : $2" - echo "--" - echo - } >&2 # output error to STDERR - return 1 - fi -} - -assert_not_empty() { - if [[ -z "$1" ]]; then - { - echo - echo "-- πŸ’₯ value is empty πŸ’₯ --" - echo "value : $1" - echo "--" - echo - } >&2 # output error to STDERR - return 1 - fi -} diff --git a/.github/workflows/e2e/bats/verify.bats b/.github/workflows/e2e/bats/verify.bats deleted file mode 100644 index 342ae02b0..000000000 --- a/.github/workflows/e2e/bats/verify.bats +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env bats - -load utilities - -GO_SCOPE="go.opentelemetry.io/auto/net/http" -JAVA_SCOPE="io.opentelemetry.tomcat-7.0" -JAVA_CLIENT_SCOPE="io.opentelemetry.http-url-connection" -JS_SCOPE="@opentelemetry/instrumentation-http" - -@test "all :: includes service.name in resource attributes" { - result=$(resource_attributes_received | jq "select(.key == \"service.name\").value.stringValue" | sort | uniq) - result_separated=$(echo $result | sed 's/\n/,/g') - assert_equal "$result_separated" '"coupon" "frontend" "inventory" "membership" "pricing"' -} - -@test "all :: includes odigos.version in resource attributes" { - result=$(resource_attributes_received | jq -r "select(.key == \"odigos.version\").value.stringValue") - - # Count occurrences of "e2e-test" - e2e_test_count=$(echo "$result" | grep -Fx "e2e-test" | wc -l | xargs) - - # Ensure all values match "e2e-test" by comparing counts - total_count=$(echo "$result" | wc -l | xargs) - - assert_equal "$e2e_test_count" "$total_count" - - # Ensure there are at least 5 elements in the array (currently 5 services) - assert_ge "$total_count" 5 -} - -@test "go :: emits a span name '{http.method}' (per semconv)" { - result=$(server_span_names_for ${GO_SCOPE}) - assert_equal "$result" '"GET"' -} - -@test "java :: emits a span name '{http.method} {http.route}''" { - result=$(server_span_names_for ${JAVA_SCOPE} | sort) - result_separated=$(echo $result | sed 's/\n/,/g') - assert_equal "$result_separated" '"GET /price" "POST /buy"' -} - -@test "js :: emits a span name '{http.method}' (per semconv)" { - result=$(server_span_names_for ${JS_SCOPE}) - assert_equal "$result" '"POST"' -} - -@test "go :: includes http.request.method attribute" { - result=$(server_span_attributes_for ${GO_SCOPE} | jq "select(.key == \"http.request.method\").value.stringValue") - assert_equal "$result" '"GET"' -} - -@test "java :: includes http.request.method attribute" { - result=$(server_span_attributes_for ${JAVA_SCOPE} | jq "select(.key == \"http.request.method\").value.stringValue" | sort) - result_separated=$(echo $result | sed 's/\n/,/g') - assert_equal "$result_separated" '"GET" "POST"' -} - -@test "js :: includes http.method attribute" { - result=$(server_span_attributes_for ${JS_SCOPE} | jq "select(.key == \"http.method\").value.stringValue") - assert_equal "$result" '"POST"' -} - -@test "go :: includes url.path attribute" { - result=$(server_span_attributes_for ${GO_SCOPE} | jq "select(.key == \"url.path\").value.stringValue") - assert_equal "$result" '"/isMember"' -} - -@test "java :: includes url.path attributes" { - result=$(server_span_attributes_for ${JAVA_SCOPE} | jq "select(.key == \"url.path\").value.stringValue" | sort) - result_separated=$(echo $result | sed 's/\n/,/g') - assert_equal "$result_separated" '"/buy" "/price"' -} - -@test "js :: includes http.target attribute" { - result=$(server_span_attributes_for ${JS_SCOPE} | jq "select(.key == \"http.target\").value.stringValue") - assert_equal "$result" '"/apply-coupon"' -} - -@test "go :: includes http.response.status_code attribute" { - result=$(server_span_attributes_for ${GO_SCOPE} | jq "select(.key == \"http.response.status_code\").value.intValue") - assert_equal "$result" '"200"' -} - -@test "java :: includes http.response.status_code attribute" { - result=$(server_span_attributes_for ${JAVA_SCOPE} | jq "select(.key == \"http.response.status_code\").value.intValue" | sort) - result_separated=$(echo $result | sed 's/\n/,/g') - assert_equal "$result_separated" '"200" "200"' -} - -@test "js :: includes http.status_code attribute" { - result=$(server_span_attributes_for ${JS_SCOPE} | jq "select(.key == \"http.status_code\").value.intValue") - assert_equal "$result" '"200"' -} - -@test "client :: includes http.response.status_code attribute" { - result=$(client_span_attributes_for ${JAVA_CLIENT_SCOPE} | jq "select(.key == \"http.response.status_code\").value.intValue" | sort) - result_separated=$(echo $result | sed 's/\n/,/g') - assert_equal "$result_separated" '"200" "200" "200"' -} - -@test "server :: trace ID present and valid in all spans" { - trace_id=$(server_spans_from_scope_named ${GO_SCOPE} | jq ".traceId") - assert_regex "$trace_id" ${MATCH_A_TRACE_ID} - trace_ids=$(server_spans_from_scope_named ${JAVA_SCOPE} | jq ".traceId") - while read -r line; do - assert_regex "$line" ${MATCH_A_TRACE_ID} - done <<< "$trace_ids" - trace_ids=$(server_spans_from_scope_named ${JS_SCOPE} | jq ".traceId") - while read -r line; do - assert_regex "$line" ${MATCH_A_TRACE_ID} - done <<< "$trace_ids" -} - -@test "server :: span ID present and valid in all spans" { - span_id=$(server_spans_from_scope_named ${GO_SCOPE} | jq ".spanId") - assert_regex "$span_id" ${MATCH_A_SPAN_ID} - span_ids=$(server_spans_from_scope_named ${JAVA_SCOPE} | jq ".spanId") - while read -r line; do - assert_regex "$line" ${MATCH_A_SPAN_ID} - done <<< "$span_ids" - span_ids=$(server_spans_from_scope_named ${JS_SCOPE} | jq ".spanId") - while read -r line; do - assert_regex "$line" ${MATCH_A_SPAN_ID} - done <<< "$span_ids" -} - -@test "server :: parent span ID present and valid in all spans" { - parent_span_id=$(server_spans_from_scope_named ${GO_SCOPE} | jq ".parentSpanId") - assert_regex "$parent_span_id" ${MATCH_A_SPAN_ID} - parent_span_ids=$(server_spans_from_scope_named ${JAVA_SCOPE} | jq ".parentSpanId" | sort) - while read -r line; do - assert_regex "$line" ${MATCH_A_SPAN_ID} - done <<< "$parent_span_ids" - parent_span_ids=$(server_spans_from_scope_named ${JS_SCOPE} | jq ".parentSpanId" | sort) - while read -r line; do - assert_regex "$line" ${MATCH_A_SPAN_ID} - done <<< "$parent_span_ids" -} - -@test "client, server :: spans have same trace ID" { - client_trace_id=$(client_spans_from_scope_named ${JAVA_CLIENT_SCOPE} | jq ".traceId" | uniq) - assert_not_empty "$client_trace_id" - server_trace_id=$(server_spans_from_scope_named ${JAVA_SCOPE} | jq ".traceId" | uniq) - assert_not_empty "$server_trace_id" - assert_equal "$server_trace_id" "$client_trace_id" -} - -@test "client, server :: server span has client span as parent" { - server_parent_span_ids=$(server_spans_from_scope_named ${JAVA_SCOPE} | jq ".parentSpanId" | sort) - client_span_ids=$(client_spans_from_scope_named ${JAVA_CLIENT_SCOPE} | jq ".spanId" | sort) - # Verify client_span_ids is contained in server_parent_span_ids - while read -r line; do - if [[ "$client_span_ids" != *"$line"* ]]; then - echo "client span ID $line not found in server parent span IDs" - exit 1 - fi - done <<< "$server_parent_span_ids" - - # Verify Go server span has JS client span as parent - go_parent_span_id=$(server_spans_from_scope_named ${GO_SCOPE} | jq ".parentSpanId") - assert_not_empty "$go_parent_span_id" - js_client_span_id=$(client_spans_from_scope_named ${JS_SCOPE} | jq ".spanId") - assert_not_empty "$js_client_span_id" - assert_equal "$go_parent_span_id" "$js_client_span_id" -} \ No newline at end of file diff --git a/.github/workflows/e2e/collector-helm-values.yaml b/.github/workflows/e2e/collector-helm-values.yaml deleted file mode 100644 index 5b3f81a2c..000000000 --- a/.github/workflows/e2e/collector-helm-values.yaml +++ /dev/null @@ -1,49 +0,0 @@ -mode: "statefulset" - -config: - receivers: - otlp: - protocols: - http: - endpoint: ${env:MY_POD_IP}:4318 - - exporters: - debug: {} - file/trace: - path: /tmp/trace.json - rotation: - - service: - telemetry: - logs: - level: "debug" - pipelines: - traces: - receivers: - - otlp - exporters: - - file/trace - - debug - - -image: - repository: otel/opentelemetry-collector-contrib - tag: "latest" - -command: - name: otelcol-contrib - -extraVolumes: - - name: filevolume - emptyDir: {} -extraVolumeMounts: - - mountPath: /tmp - name: filevolume - -extraContainers: - - name: filecp - image: busybox - command: ["sh", "-c", "sleep 36000"] - volumeMounts: - - name: filevolume - mountPath: /tmp \ No newline at end of file diff --git a/.github/workflows/e2e/jaeger-dest.yaml b/.github/workflows/e2e/jaeger-dest.yaml deleted file mode 100644 index aa2e5bd93..000000000 --- a/.github/workflows/e2e/jaeger-dest.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: odigos.io/v1alpha1 -kind: Destination -metadata: - generateName: odigos.io.dest.jaeger- - namespace: odigos-system -spec: - data: - JAEGER_URL: test-opentelemetry-collector.traces:4317 - destinationName: collector - signals: - - TRACES - type: jaeger \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52a05aef1..80ccc3279 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release Odigos CLI +name: Release Odigos on: workflow_dispatch: @@ -137,6 +137,7 @@ jobs: echo "TAG=${{ github.event.client_payload.tag }}" >> $GITHUB_ENV else echo "Unknown event type" + echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV exit 1 fi @@ -156,4 +157,6 @@ jobs: version: v3.15.2 - name: Release Helm charts - run: sh ./scripts/release-charts.sh + env: + GH_TOKEN: ${{ github.token }} + run: bash ./scripts/release-charts.sh diff --git a/.gitignore b/.gitignore index dca09e610..9f48db9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,4 @@ dist/ node_modules .DS_Store go.work.sum -opentelemetry-helm-charts/ -odigos-e2e-test -.github/workflows/e2e/bats/traces-orig.json -.github/workflows/e2e/bats/traces.json cli/odigos diff --git a/Makefile b/Makefile index b5633f379..254883859 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ build-odiglet-with-agents: docker build -t $(ORG)/odigos-odiglet:$(TAG) . -f odiglet/Dockerfile --build-arg ODIGOS_VERSION=$(TAG) --build-context nodejs-agent-native-community-src=../opentelemetry-node .PHONY: build-autoscaler -build-autoscaler: +build-autoscaler: docker build -t $(ORG)/odigos-autoscaler:$(TAG) . --build-arg SERVICE_NAME=autoscaler .PHONY: build-instrumentor @@ -39,12 +39,7 @@ build-ui: .PHONY: build-images build-images: - make build-autoscaler TAG=$(TAG) - make build-scheduler TAG=$(TAG) - make build-odiglet TAG=$(TAG) - make build-instrumentor TAG=$(TAG) - make build-collector TAG=$(TAG) - make build-ui TAG=$(TAG) + make -j 3 build-autoscaler build-scheduler build-odiglet build-instrumentor build-collector build-ui TAG=$(TAG) .PHONY: push-odiglet push-odiglet: @@ -100,13 +95,8 @@ load-to-kind-scheduler: .PHONY: load-to-kind load-to-kind: - make load-to-kind-autoscaler TAG=$(TAG) - make load-to-kind-scheduler TAG=$(TAG) - make load-to-kind-odiglet TAG=$(TAG) - kind load docker-image $(ORG)/odigos-instrumentor:$(TAG) - make load-to-kind-collector TAG=$(TAG) - make load-to-kind-ui TAG=$(TAG) - make load-to-kind-scheduler TAG=$(TAG) + make -j 6 load-to-kind-instrumentor load-to-kind-autoscaler load-to-kind-scheduler load-to-kind-odiglet load-to-kind-collector load-to-kind-ui TAG=$(TAG) + .PHONY: restart-ui restart-ui: diff --git a/agents/python/configurator/__init__.py b/agents/python/configurator/__init__.py index 1969eb689..1fdc74683 100644 --- a/agents/python/configurator/__init__.py +++ b/agents/python/configurator/__init__.py @@ -1,18 +1,20 @@ -# my_otel_configurator/__init__.py -import opentelemetry.sdk._configuration as sdk_config import threading import atexit import os +import opentelemetry.sdk._configuration as sdk_config from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.resources import ProcessResourceDetector, OTELResourceDetector +from .lib_handling import reorder_python_path, reload_distro_modules from .version import VERSION from opamp.http_client import OpAMPHTTPClient class OdigosPythonConfigurator(sdk_config._BaseConfigurator): + def _configure(self, **kwargs): _initialize_components() - -def _initialize_components(): + + +def _initialize_components(): trace_exporters, metric_exporters, log_exporters = sdk_config._import_exporters( sdk_config._get_exporter_names("traces"), sdk_config._get_exporter_names("metrics"), @@ -42,6 +44,12 @@ def _initialize_components(): initialize_logging_if_enabled(log_exporters, resource) + # Reorder the python sys.path to ensure that the user application's dependencies take precedence over the agent's dependencies. + # This is necessary because the user application's dependencies may be incompatible with those used by the agent. + reorder_python_path() + # Reload distro modules to ensure the new path is used. + reload_distro_modules() + def initialize_traces_if_enabled(trace_exporters, resource): traces_enabled = os.getenv(sdk_config.OTEL_TRACES_EXPORTER, "none").strip().lower() if traces_enabled != "none": diff --git a/agents/python/configurator/lib_handling.py b/agents/python/configurator/lib_handling.py new file mode 100644 index 000000000..31469900f --- /dev/null +++ b/agents/python/configurator/lib_handling.py @@ -0,0 +1,36 @@ +import sys +import importlib +from importlib import metadata as md + +def reorder_python_path(): + paths_to_move = [path for path in sys.path if path.startswith('/var/odigos/python')] + + for path in paths_to_move: + sys.path.remove(path) + sys.path.append(path) + + +def reload_distro_modules() -> None: + # Reload distro modules, as they may have been imported before the path was reordered. + # Add any new distro modules to this list. + needed_modules = [ + 'google.protobuf', + 'requests', + 'charset_normalizer', + 'certifi', + 'asgiref' + 'idna', + 'deprecated', + 'importlib_metadata', + 'packaging', + 'psutil', + 'zipp', + 'urllib3', + 'uuid_extensions.uuid7', + 'typing_extensions', + ] + + for module_name in needed_modules: + if module_name in sys.modules: + module = sys.modules[module_name] + importlib.reload(module) diff --git a/api/config/crd/bases/odigos.io_instrumentationinstances.yaml b/api/config/crd/bases/odigos.io_instrumentationinstances.yaml index 495df4ab6..7ef17c6ec 100644 --- a/api/config/crd/bases/odigos.io_instrumentationinstances.yaml +++ b/api/config/crd/bases/odigos.io_instrumentationinstances.yaml @@ -38,6 +38,14 @@ spec: metadata: type: object spec: + properties: + containerName: + description: |- + stores the name of the container in the pod where the SDK is running. + The pod details can be found as the owner reference on the CR. + type: string + required: + - containerName type: object status: description: |- diff --git a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstance.go b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstance.go index e0b7c1f3b..969675f1a 100644 --- a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstance.go +++ b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstance.go @@ -18,7 +18,6 @@ limitations under the License. package v1alpha1 import ( - v1alpha1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" @@ -29,7 +28,7 @@ import ( type InstrumentationInstanceApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - Spec *v1alpha1.InstrumentationInstanceSpec `json:"spec,omitempty"` + Spec *InstrumentationInstanceSpecApplyConfiguration `json:"spec,omitempty"` Status *InstrumentationInstanceStatusApplyConfiguration `json:"status,omitempty"` } @@ -205,8 +204,8 @@ func (b *InstrumentationInstanceApplyConfiguration) ensureObjectMetaApplyConfigu // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. -func (b *InstrumentationInstanceApplyConfiguration) WithSpec(value v1alpha1.InstrumentationInstanceSpec) *InstrumentationInstanceApplyConfiguration { - b.Spec = &value +func (b *InstrumentationInstanceApplyConfiguration) WithSpec(value *InstrumentationInstanceSpecApplyConfiguration) *InstrumentationInstanceApplyConfiguration { + b.Spec = value return b } diff --git a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstancespec.go b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstancespec.go new file mode 100644 index 000000000..e12b1cce0 --- /dev/null +++ b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstancespec.go @@ -0,0 +1,38 @@ +/* +Copyright 2022. + +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 + + http://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. +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// InstrumentationInstanceSpecApplyConfiguration represents an declarative configuration of the InstrumentationInstanceSpec type for use +// with apply. +type InstrumentationInstanceSpecApplyConfiguration struct { + ContainerName *string `json:"containerName,omitempty"` +} + +// InstrumentationInstanceSpecApplyConfiguration constructs an declarative configuration of the InstrumentationInstanceSpec type for use with +// apply. +func InstrumentationInstanceSpec() *InstrumentationInstanceSpecApplyConfiguration { + return &InstrumentationInstanceSpecApplyConfiguration{} +} + +// WithContainerName sets the ContainerName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ContainerName field is set to the value of the last call. +func (b *InstrumentationInstanceSpecApplyConfiguration) WithContainerName(value string) *InstrumentationInstanceSpecApplyConfiguration { + b.ContainerName = &value + return b +} diff --git a/api/generated/odigos/applyconfiguration/utils.go b/api/generated/odigos/applyconfiguration/utils.go index a9ee7510d..a619c840c 100644 --- a/api/generated/odigos/applyconfiguration/utils.go +++ b/api/generated/odigos/applyconfiguration/utils.go @@ -54,6 +54,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &odigosv1alpha1.InstrumentationConfigSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("InstrumentationInstance"): return &odigosv1alpha1.InstrumentationInstanceApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("InstrumentationInstanceSpec"): + return &odigosv1alpha1.InstrumentationInstanceSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("InstrumentationInstanceStatus"): return &odigosv1alpha1.InstrumentationInstanceStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("InstrumentationLibrary"): diff --git a/api/odigos/v1alpha1/instrumentationinstance_types.go b/api/odigos/v1alpha1/instrumentationinstance_types.go index e19bfd9eb..7148427bf 100644 --- a/api/odigos/v1alpha1/instrumentationinstance_types.go +++ b/api/odigos/v1alpha1/instrumentationinstance_types.go @@ -21,6 +21,10 @@ import ( ) type InstrumentationInstanceSpec struct { + // +required + // stores the name of the container in the pod where the SDK is running. + // The pod details can be found as the owner reference on the CR. + ContainerName string `json:"containerName"` } // +kubebuilder:validation:Enum=instrumentation;sampler;exporter @@ -79,26 +83,32 @@ type InstrumentationLibraryStatus struct { // InstrumentationInstanceStatus defines the observed state of InstrumentationInstance // If the instrumentation is not active, this CR should be deleted type InstrumentationInstanceStatus struct { + // Attributes that identify the SDK and are reported as resource attributes in the generated telemetry. // One can identify if an arbitrary telemetry is generated by this SDK by checking those resource attributes. IdentifyingAttributes []Attribute `json:"identifyingAttributes,omitempty"` + // Attributes that are not reported as resource attributes but useful to describe characteristics of the SDK. NonIdentifyingAttributes []Attribute `json:"nonIdentifyingAttributes,omitempty"` Healthy *bool `json:"healthy,omitempty"` + // message is a human readable message indicating details about the SDK general health. // can be omitted if healthy is true // +kubebuilder:validation:MaxLength=32768 Message string `json:"message,omitempty"` + // reason contains a programmatic identifier indicating the reason for the component status. // Producers of specific condition types may define expected values and meanings for this field, // and whether the values are considered a guaranteed API. Reason string `json:"reason,omitempty"` + // +required // +kubebuilder:validation:Required // +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=date-time - LastStatusTime metav1.Time `json:"lastStatusTime"` - Components []InstrumentationLibraryStatus `json:"components,omitempty"` + LastStatusTime metav1.Time `json:"lastStatusTime"` + + Components []InstrumentationLibraryStatus `json:"components,omitempty"` } //+genclient diff --git a/cli/cmd/describe.go b/cli/cmd/describe.go index 0374ee38c..f0bef2bf6 100644 --- a/cli/cmd/describe.go +++ b/cli/cmd/describe.go @@ -405,6 +405,9 @@ func printPodContainerInfo(pod *corev1.Pod, container *corev1.Container, instrum if instance.OwnerReferences[0].Name != pod.GetName() { continue } + if instance.Spec.ContainerName != container.Name { + continue + } thisPodInstrumentationInstances = append(thisPodInstrumentationInstances, &instance) } printPodContainerInstrumentationInstancesInfo(thisPodInstrumentationInstances) diff --git a/common/config/clickhouse.go b/common/config/clickhouse.go index 23ee4c756..3ed3ad5f9 100644 --- a/common/config/clickhouse.go +++ b/common/config/clickhouse.go @@ -2,14 +2,21 @@ package config import ( "errors" + "net/url" + "strings" "github.com/odigos-io/odigos/common" ) const ( - clickhouseEndpoint = "CLICKHOUSE_ENDPOINT" - clickhouseUsername = "CLICKHOUSE_USERNAME" - clickhousePassword = "CLICKHOUSE_PASSWORD" + clickhouseEndpoint = "CLICKHOUSE_ENDPOINT" + clickhouseUsername = "CLICKHOUSE_USERNAME" + clickhousePassword = "${CLICKHOUSE_PASSWORD}" + clickhouseCreateSchema = "CLICKHOUSE_CREATE_SCHEME" + clickhouseDatabaseName = "CLICKHOUSE_DATABASE_NAME" + clickhouseTracesTable = "CLICKHOUSE_TRACES_TABLE" + clickhouseMetricsTable = "CLICKHOUSE_METRICS_TABLE" + clickhouseLogsTable = "CLICKHOUSE_LOGS_TABLE" ) type Clickhouse struct{} @@ -24,19 +31,53 @@ func (c *Clickhouse) ModifyConfig(dest ExporterConfigurer, currentConfig *Config return errors.New("clickhouse endpoint not specified, gateway will not be configured for Clickhouse") } - username, userExists := dest.GetConfig()[clickhouseUsername] - password, passExists := dest.GetConfig()[clickhousePassword] - if userExists != passExists { - return errors.New("clickhouse username and password must be both specified, or neither") + if !strings.Contains(endpoint, "://") { + endpoint = "tcp://" + endpoint + } + + parsedUrl, err := url.Parse(endpoint) + if err != nil { + return errors.New("clickhouse endpoint is not a valid URL") + } + + if parsedUrl.Port() == "" { + endpoint = strings.Replace(endpoint, parsedUrl.Host, parsedUrl.Host+":9000", 1) } + username, userExists := dest.GetConfig()[clickhouseUsername] + exporterName := "clickhouse/clickhouse-" + dest.GetID() exporterConfig := GenericMap{ "endpoint": endpoint, } if userExists { exporterConfig["username"] = username - exporterConfig["password"] = password + exporterConfig["password"] = clickhousePassword + } + + createSchema, exists := dest.GetConfig()[clickhouseCreateSchema] + createSchemaBoolValue := exists && strings.ToLower(createSchema) == "create" + exporterConfig["create_schema"] = createSchemaBoolValue + + dbName, exists := dest.GetConfig()[clickhouseDatabaseName] + if !exists { + return errors.New("clickhouse database name not specified, gateway will not be configured for Clickhouse") + } + exporterConfig["database"] = dbName + + tracesTable, exists := dest.GetConfig()[clickhouseTracesTable] + if exists { + exporterConfig["traces_table_name"] = tracesTable + } + + metricsTable, exists := dest.GetConfig()[clickhouseMetricsTable] + if exists { + exporterConfig["metrics_table_name"] = metricsTable + } + + logsTable, exists := dest.GetConfig()[clickhouseLogsTable] + if exists { + exporterConfig["logs_table_name"] = logsTable } currentConfig.Exporters[exporterName] = exporterConfig diff --git a/destinations/data/clickhouse.yaml b/destinations/data/clickhouse.yaml index 8b4542c81..5eac44faf 100644 --- a/destinations/data/clickhouse.yaml +++ b/destinations/data/clickhouse.yaml @@ -29,7 +29,44 @@ spec: - name: CLICKHOUSE_PASSWORD displayName: Password componentType: input + secret: true componentProps: type: password required: false - secret: true + - name: CLICKHOUSE_CREATE_SCHEME + displayName: Create Scheme + componentType: dropdown + componentProps: + values: + - Create + - Skip + required: true + initialValue: Create + - name: CLICKHOUSE_DATABASE_NAME + displayName: Database Name + componentType: input + componentProps: + type: text + required: true + initialValue: otel + - name: CLICKHOUSE_TRACES_TABLE + displayName: Traces Table + componentType: input + componentProps: + type: text + required: true + initialValue: otel_traces + - name: CLICKHOUSE_METRICS_TABLE + displayName: Metrics Table + componentType: input + componentProps: + type: text + required: true + initialValue: otel_metrics + - name: CLICKHOUSE_LOGS_TABLE + displayName: Logs Table + componentType: input + componentProps: + type: text + required: true + initialValue: otel_logs \ No newline at end of file diff --git a/docs/instrumentations/golang/golang.mdx b/docs/instrumentations/golang/golang.mdx index 0088d1649..d58722106 100644 --- a/docs/instrumentations/golang/golang.mdx +++ b/docs/instrumentations/golang/golang.mdx @@ -5,7 +5,7 @@ sidebarTitle: "Go" ## Supported Versions -Odigos uses the official [opentelemetry-go-instrumentation](https://github.com/open-telemetry/opentelemetry-go-instrumentation) OpenTelemetry Auto Instrumentation using eBPF, thus it supports the same Node.js versions as this project. +Odigos uses the official [opentelemetry-go-instrumentation](https://github.com/open-telemetry/opentelemetry-go-instrumentation) OpenTelemetry Auto Instrumentation using eBPF, thus it supports the same golang versions as this project. - Go runtime versions **1.17** and above are supported. diff --git a/docs/instrumentations/nodejs/enrichment.mdx b/docs/instrumentations/nodejs/enrichment.mdx index a4c29405f..5033c37c0 100644 --- a/docs/instrumentations/nodejs/enrichment.mdx +++ b/docs/instrumentations/nodejs/enrichment.mdx @@ -62,5 +62,17 @@ function my_function() { Make sure to replace `instrumentation-scope-name` and `instrumentation-scope-version` with the name and version of your instrumented file. +Important Notes: + +1. **Always End a span**: + Ensure that every span is ended to appear in your trace. Defer the End method of the span to guarantee that the span is always ended, even with multiple return paths in the function. +2. **Propagate and use a valid context object**: + When calling tracer.Start, use a valid context object instead of context.Background(). This makes the new span a child of the active span, ensuring it appears correctly in the trace. +3. **Pass the context object downstream**: + When calling downstream functions, pass the context object returned from tracer.Start() to ensure any spans created within these functions are children of the current span. This maintains the hierarchical relationship between spans and provides a clear trace of the request flow. +4. **Assign meaningful names to spans**: + Use descriptive names for spans, (such as the function name) to clearly describe the operations they represent. This helps anyone examining the trace to easily understand the span's purpose and context. +5. **Avoid dynamic, high cardinality data in span names**: + Do not include dynamic data such as IDs in the span name, as this can cause performance issues and make the trace harder to read. Instead, use static, descriptive names for spans and record dynamic information in span attributes. This ensures better performance and readability of the trace. diff --git a/docs/instrumentations/python/enrichment.mdx b/docs/instrumentations/python/enrichment.mdx new file mode 100644 index 000000000..2510ef09b --- /dev/null +++ b/docs/instrumentations/python/enrichment.mdx @@ -0,0 +1,96 @@ + +--- +title: "Enriching Python OpenTelemetry Data" +sidebarTitle: "Enrichment" +--- + +Odigos will automatically instrument your Python services and record semantic spans from popular modules. +Many users find the automatic instrumentation data sufficient for their needs. However, if there is anything specific to your application that you want to record, you can enrich the data by adding custom spans to your code. + +This is sometimes referred to as "manual instrumentation" + +## Required dependencies + +Install the API from PyPI using pip: + +```bash +pip install opentelemetry-api +``` + +## Creating Spans + +To create a span, use the `tracer` object from the `opentelemetry.trace` module. The `tracer` object is a factory for creating spans. + +```python +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +def my_function(): + with tracer.start_as_current_span("my_function") as span: + print("Hello world!") +``` + +Important Notes: + +1. **Assign meaningful names to spans**: + Use descriptive names for spans, (such as the function name) to clearly describe the operations they represent. This helps anyone examining the trace to easily understand the span's purpose and context. +2. **Avoid dynamic, high cardinality data in span names**: + Do not include dynamic data such as IDs in the span name, as this can cause performance issues and make the trace harder to read. Instead, use static, descriptive names for spans and record dynamic information in span attributes. This ensures better performance and readability of the trace. + + + +### Recording Span Attributes + +Span attributes are key-value pairs that record additional information about an operation, which can be useful for debugging, performance analysis, or troubleshooting + +Examples: + +- User ID, organization ID, Account ID or other identifiers. +- Inputs - the relevant parameters or configuration that influenced the operation. +- Outputs - the result of the operation. +- Entities - the entities or resources that the operation interacted with. + +Attribute names are lowercased strings in the form `my_application.my_attribute`, example: `my_service.user_id`. +Read more [here](https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/) + +To record attributes, use the `set_attribute` method on the span object. + +``` python +def my_function(arg: str): + with tracer.start_as_current_span("my_function") as span: + span.set_attribute("argument_name", arg) +``` + +Important Notes: + +1. **Be cautious when recording data**: + Avoid including PII (personally identifiable information) or any data you do not wish to expose in your traces. +2. **Attribute cost considerations**: + Each attribute affects performance and processing time. Record only what is necessary and avoid superfluous data. +3. **Use static names for attributes**: + Avoid dynamic content such as IDs in attribute keys. Use static names and properly namespace them (scope.attribute_name) to provide clear context for downstream consumers. +4. **Adhere to OpenTelemetry semantic conventions**: + Prefer using namespaces and attribute names from the OpenTelemetry semantic conventions to enhance data interoperability and make it easier for others to understand. + + +## Recording Errors + +To easily identify and monitor errors in your traces, the Span object includes a status field that can be used to mark the span as an error. This helps in spotting errors in trace viewers, sampling, and setting up alerts. + +If an operation you are tracing fails, you can mark the span's status as an error and record the error details within the span. Here's how you can do it: + +- An exception has been raised to demonstrate an error that occurred in your code. + +``` python + +def my_function(): + with tracer.start_as_current_span("my_function") as span: + try: + print("Hello world!") + raise Exception("Some Exception") + except Exception as e: + span.record_exception(e) + span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) + +``` \ No newline at end of file diff --git a/docs/instrumentations/python/python.mdx b/docs/instrumentations/python/python.mdx new file mode 100644 index 000000000..bb70d05c1 --- /dev/null +++ b/docs/instrumentations/python/python.mdx @@ -0,0 +1,88 @@ +--- +title: "Python Automatic Instrumentation" +sidebarTitle: "Python" +--- + +## Supported Versions + +Odigos uses the official [opentelemetry-python-instrumentation](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation) OpenTelemetry Auto Instrumentation, thus it supports the same Python versions as this project. + +- In the enterprise version, Odigos leverages eBPF to enhance performance in the Python instrumentation process. + +- Python runtime versions 3.8 and above are supported. + + +## Traces + +Odigos will automatically instrument your Python services to record and collect spans for distributed tracing, by utilizing the OpenTelemetry Python official auto Instrumentation Libraries. + +## Instrumentation Libraries + +The following Python modules will be auto instrumented by Odigos: + +### Database Clients, ORMs, and Data Access Libraries +- [`aiopg`](https://pypi.org/project/aiopg/) versions `aiopg >= 0.13.0, < 2.0.0` +- [`dbapi`](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/dbapi/dbapi.html) +- [`mysql`](https://pypi.org/project/mysql-connector-python/) version `mysql-connector-python >= 8.0.0, < 9.0.0` +- [`mysqlclient`](https://pypi.org/project/mysqlclient/) version `mysqlclient < 3.0.0` +- [`psycopg`](https://pypi.org/project/psycopg/) versions `psycopg >= 3.1.0` +- [`psycopg2`](https://pypi.org/project/psycopg2/) versions `psycopg2 >= 2.7.3.1` +- [`pymemcache`](https://pypi.org/project/pymemcache/) versions `pymemcache >= 1.3.5, < 5.0.0` +- [`pymongo`](https://pypi.org/project/pymongo/) versions `pymongo >= 3.1, < 5.0.0` +- [`pymysql`](https://pypi.org/project/PyMySQL/) versions `pymysql < 2.0.0` +- [`redis`](https://pypi.org/project/redis/) versions `redis >= 2.6` +- [`sqlalchemy`](https://pypi.org/project/SQLAlchemy/) +- [`sqlite3`](https://docs.python.org/3/library/sqlite3.html) +- [`tortoiseorm`](https://pypi.org/project/tortoise-orm/) versions `tortoise-orm >= 0.17.0`, `pydantic >= 1.10.2` +- [`cassandra`](https://pypi.org/project/cassandra-driver/) versions `cassandra-driver >= 3.25.0, < 4.0.0`, `scylla-driver >= 3.25.0, < 4.0.0` +- [`elasticsearch`](https://pypi.org/project/elasticsearch/) versions `elasticsearch >= 6.0.0` +- [`asyncpg`](https://pypi.org/project/asyncpg/) versions `asyncpg >= 0.12.0` + +### HTTP Frameworks +- [`asgi`](https://pypi.org/project/asgiref/) versions `asgiref >= 3.0.0, < 4.0.0` +- [`django`](https://pypi.org/project/Django/) versions `django >= 1.10.0` +- [`fastapi`](https://pypi.org/project/fastapi/) versions `fastapi >= 0.58.0, < 0.59.0`, `fastapi-slim >= 0.111.0, < 0.112.0` +- [`flask`](https://pypi.org/project/Flask/) versions `flask >= 1.0.0` +- [`pyramid`](https://pypi.org/project/pyramid/) versions `pyramid >= 1.7.0` +- [`starlette`](https://pypi.org/project/starlette/) versions `starlette >= 0.13.0, < 0.14.0` +- [`falcon`](https://pypi.org/project/falcon/) versions `falcon >= 1.4.1, < 3.1.2` +- [`tornado`](https://pypi.org/project/tornado/) versions `tornado >= 5.1.1` + +### HTTP Clients +- [`aiohttp-client`](https://pypi.org/project/aiohttp/) versions `aiohttp >= 3.0.0, < 4.0.0` +- [`httpx`](https://pypi.org/project/httpx/) versions `httpx >= 0.18.0` +- [`requests`](https://pypi.org/project/requests/) versions `requests >= 2.0.0, < 3.0.0` +- [`urllib`](https://docs.python.org/3/library/urllib.html) +- [`urllib3`](https://pypi.org/project/urllib3/) versions `urllib3 >= 1.0.0, < 3.0.0` + +### Messaging Systems Clients +- [`aio-pika`](https://pypi.org/project/aio-pika/) versions `aio_pika >= 7.2.0, < 10.0.0` +- [`celery`](https://pypi.org/project/celery/) versions `celery >= 4.0.0, < 6.0.0` +- [`confluent-kafka`](https://pypi.org/project/confluent-kafka/) versions `confluent-kafka >= 1.8.2, <= 2.4.0` +- [`kafka-python`](https://pypi.org/project/kafka-python/) versions `kafka-python >= 2.0.0` +- [`pika`](https://pypi.org/project/pika/) versions `pika >= 0.12.0` +- [`remoulade`](https://pypi.org/project/remoulade/) versions `remoulade >= 0.50.0` + +### RPC (Remote Procedure Call) +- [`grpc`](https://pypi.org/project/grpcio/) versions `grpcio >= 1.27.0, < 2.0.0` + +### Web Servers +- [`aiohttp-server`](https://pypi.org/project/aiohttp/) versions `aiohttp >= 3.0.0, < 4.0.0` +- [`wsgi`](https://docs.python.org/3/library/wsgiref.html) + +### Cloud Services and SDKs +- [`boto`](https://pypi.org/project/boto/) versions `boto >= 2.0.0, < 3.0.0` +- [`boto3sqs`](https://pypi.org/project/boto3/) versions `boto3 >= 1.0.0, < 2.0.0` +- [`botocore`](https://pypi.org/project/botocore/) versions `botocore >= 1.0.0, < 2.0.0` + +### Framework and Library Utilities +- [`jinja2`](https://pypi.org/project/Jinja2/) versions `jinja2 >= 2.7, < 4.0` + +### Other +- [`asyncio`](https://pypi.org/project/asyncio/) + +### Loggers + +Automatic injection of trace context (trace id and span id) into log records for the following loggers: + +- [`logging`](https://docs.python.org/3/library/logging.html) \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 65d2e526c..86f098c96 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -105,7 +105,14 @@ "instrumentations/nodejs/nodejs", "instrumentations/nodejs/enrichment" ] - } + }, + { + "group": "Python", + "pages": [ + "instrumentations/python/python", + "instrumentations/python/enrichment" + ] + } ] }, { diff --git a/docs/pipeline/actions/attributes/piimasking.mdx b/docs/pipeline/actions/attributes/piimasking.mdx index 8eb616ea9..989ecc301 100644 --- a/docs/pipeline/actions/attributes/piimasking.mdx +++ b/docs/pipeline/actions/attributes/piimasking.mdx @@ -1,6 +1,6 @@ --- -title: "Pii Masking" -sidebarTitle: "Pii Masking" +title: "PII Masking" +sidebarTitle: "PII Masking" --- The "PII Masking" Odigos Action can be used to mask PII data from span attribute values. diff --git a/docs/pipeline/actions/sampling/introduction.mdx b/docs/pipeline/actions/sampling/introduction.mdx index 845701cb1..d51e8a917 100644 --- a/docs/pipeline/actions/sampling/introduction.mdx +++ b/docs/pipeline/actions/sampling/introduction.mdx @@ -2,8 +2,10 @@ title: "Sampling Actions Introduction" sidebarTitle: "Introduction" --- -> **Note:** -> This feature is in beta. It may be subject to changes and improvements based on user feedback. + + +This feature is in beta. It may be subject to changes and improvements based on user feedback. + Sampling Actions allow you to configure various types of sampling methods before exporting traces to your Odigos Destinations. diff --git a/frontend/endpoints/actual-sources.go b/frontend/endpoints/actual-sources.go index e11d0c32a..237dbff86 100644 --- a/frontend/endpoints/actual-sources.go +++ b/frontend/endpoints/actual-sources.go @@ -14,7 +14,14 @@ import ( ) func GetActualSources(ctx context.Context, odigosns string) []ThinSource { + return getSourcesForNamespace(ctx, odigosns) +} + +func GetNamespaceActualSources(ctx context.Context, namespace string) []ThinSource { + return getSourcesForNamespace(ctx, namespace) +} +func getSourcesForNamespace(ctx context.Context, namespace string) []ThinSource { effectiveInstrumentedSources := map[SourceID]ThinSource{} var ( @@ -24,7 +31,7 @@ func GetActualSources(ctx context.Context, odigosns string) []ThinSource { g, ctx := errgroup.WithContext(ctx) g.Go(func() error { - relevantNamespaces, err := getRelevantNameSpaces(ctx, odigosns) + relevantNamespaces, err := getRelevantNameSpaces(ctx, namespace) if err != nil { return err } @@ -32,9 +39,6 @@ func GetActualSources(ctx context.Context, odigosns string) []ThinSource { for _, ns := range relevantNamespaces { nsInstrumentedMap[ns.Name] = isObjectLabeledForInstrumentation(ns.ObjectMeta) } - // get all the applications in all the namespaces, - // passing an empty string here is more efficient compared to iterating over the namespaces - // since it will make a single request per workload type to the k8s api server items, err = getApplicationsInNamespace(ctx, "", nsInstrumentedMap) return err }) @@ -60,10 +64,6 @@ func GetActualSources(ctx context.Context, odigosns string) []ThinSource { } sourcesResult := []ThinSource{} - // go over the instrumented applications and update the languages of the effective sources. - // Not all effective sources necessarily have a corresponding instrumented application, - // it may take some time for the instrumented application to be created. In that case the languages - // slice will be empty. for _, app := range instrumentedApplications.Items { thinSource := k8sInstrumentedAppToThinSource(&app) if source, ok := effectiveInstrumentedSources[thinSource.SourceID]; ok { @@ -77,13 +77,10 @@ func GetActualSources(ctx context.Context, odigosns string) []ThinSource { } return sourcesResult - } func GetActualSource(ctx context.Context, ns string, kind string, name string) (*Source, error) { - k8sObjectName := workload.GetRuntimeObjectName(name, kind) - owner, numberOfRunningInstances := getWorkload(ctx, ns, kind, name) if owner == nil { return nil, fmt.Errorf("owner not found") @@ -105,9 +102,7 @@ func GetActualSource(ctx context.Context, ns string, kind string, name string) ( instrumentedApplication, err := kube.DefaultClient.OdigosClient.InstrumentedApplications(ns).Get(ctx, k8sObjectName, metav1.GetOptions{}) if err == nil { - // valid instrumented application, grab the runtime details ts.IaDetails = k8sInstrumentedAppToThinSource(instrumentedApplication).IaDetails - // potentially add a condition for healthy instrumentation instances err = addHealthyInstrumentationInstancesCondition(ctx, instrumentedApplication, &ts) if err != nil { return nil, err diff --git a/frontend/endpoints/applications.go b/frontend/endpoints/applications.go index fd9f294a6..a16df40e6 100644 --- a/frontend/endpoints/applications.go +++ b/frontend/endpoints/applications.go @@ -4,6 +4,10 @@ import ( "context" "net/http" + appsv1 "k8s.io/api/apps/v1" + + "github.com/odigos-io/odigos/k8sutils/pkg/client" + "github.com/gin-gonic/gin" "github.com/odigos-io/odigos/frontend/kube" "golang.org/x/sync/errgroup" @@ -38,7 +42,7 @@ type GetApplicationItemInNamespace struct { type GetApplicationItem struct { // namespace is used when querying all the namespaces, the response can be grouped/filtered by namespace namespace string - nsItem GetApplicationItemInNamespace + nsItem GetApplicationItemInNamespace } func GetApplicationsInNamespace(c *gin.Context) { @@ -71,6 +75,28 @@ func GetApplicationsInNamespace(c *gin.Context) { }) } +func GetApplicationsInK8SNamespace(ctx context.Context, ns string) []GetApplicationItemInNamespace { + + namespace, err := kube.DefaultClient.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}) + if err != nil { + + return nil + } + + items, err := getApplicationsInNamespace(ctx, namespace.Name, map[string]*bool{namespace.Name: isObjectLabeledForInstrumentation(namespace.ObjectMeta)}) + if err != nil { + + return nil + } + + apps := make([]GetApplicationItemInNamespace, len(items)) + for i, item := range items { + apps[i] = item.nsItem + } + + return apps +} + // getApplicationsInNamespace returns all applications in the namespace and their instrumentation status. // nsName can be an empty string to get applications in all namespaces. // nsInstrumentedMap is a map of namespace name to a boolean pointer indicating if the namespace is instrumented. @@ -126,70 +152,76 @@ func getApplicationsInNamespace(ctx context.Context, nsName string, nsInstrument } func getDeployments(namespace string, ctx context.Context) ([]GetApplicationItem, error) { - deps, err := kube.DefaultClient.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{}) + var response []GetApplicationItem + err := client.ListWithPages(client.DefaultPageSize, kube.DefaultClient.AppsV1().Deployments(namespace).List, ctx, metav1.ListOptions{}, func(deps *appsv1.DeploymentList) error { + for _, dep := range deps.Items { + appInstrumentationLabeled := isObjectLabeledForInstrumentation(dep.ObjectMeta) + response = append(response, GetApplicationItem{ + namespace: dep.Namespace, + nsItem: GetApplicationItemInNamespace{ + Name: dep.Name, + Kind: WorkloadKindDeployment, + Instances: int(dep.Status.AvailableReplicas), + AppInstrumentationLabeled: appInstrumentationLabeled, + }, + }) + } + return nil + }) + if err != nil { return nil, err } - response := make([]GetApplicationItem, len(deps.Items)) - for i, dep := range deps.Items { - appInstrumentationLabeled := isObjectLabeledForInstrumentation(dep.ObjectMeta) - response[i] = GetApplicationItem{ - namespace: dep.Namespace, - nsItem: GetApplicationItemInNamespace { - Name: dep.Name, - Kind: WorkloadKindDeployment, - Instances: int(dep.Status.AvailableReplicas), - AppInstrumentationLabeled: appInstrumentationLabeled, - }, - } - } - return response, nil } func getStatefulSets(namespace string, ctx context.Context) ([]GetApplicationItem, error) { - ss, err := kube.DefaultClient.AppsV1().StatefulSets(namespace).List(ctx, metav1.ListOptions{}) + var response []GetApplicationItem + err := client.ListWithPages(client.DefaultPageSize, kube.DefaultClient.AppsV1().StatefulSets(namespace).List, ctx, metav1.ListOptions{}, func(sss *appsv1.StatefulSetList) error { + for _, ss := range sss.Items { + appInstrumentationLabeled := isObjectLabeledForInstrumentation(ss.ObjectMeta) + response = append(response, GetApplicationItem{ + namespace: ss.Namespace, + nsItem: GetApplicationItemInNamespace{ + Name: ss.Name, + Kind: WorkloadKindStatefulSet, + Instances: int(ss.Status.ReadyReplicas), + AppInstrumentationLabeled: appInstrumentationLabeled, + }, + }) + } + return nil + }) + if err != nil { return nil, err } - response := make([]GetApplicationItem, len(ss.Items)) - for i, s := range ss.Items { - appInstrumentationLabeled := isObjectLabeledForInstrumentation(s.ObjectMeta) - response[i] = GetApplicationItem{ - namespace: s.Namespace, - nsItem: GetApplicationItemInNamespace { - Name: s.Name, - Kind: WorkloadKindStatefulSet, - Instances: int(s.Status.ReadyReplicas), - AppInstrumentationLabeled: appInstrumentationLabeled, - }, - } - } - return response, nil } func getDaemonSets(namespace string, ctx context.Context) ([]GetApplicationItem, error) { - dss, err := kube.DefaultClient.AppsV1().DaemonSets(namespace).List(ctx, metav1.ListOptions{}) + var response []GetApplicationItem + err := client.ListWithPages(client.DefaultPageSize, kube.DefaultClient.AppsV1().DaemonSets(namespace).List, ctx, metav1.ListOptions{}, func(dss *appsv1.DaemonSetList) error { + for _, ds := range dss.Items { + appInstrumentationLabeled := isObjectLabeledForInstrumentation(ds.ObjectMeta) + response = append(response, GetApplicationItem{ + namespace: ds.Namespace, + nsItem: GetApplicationItemInNamespace{ + Name: ds.Name, + Kind: WorkloadKindDaemonSet, + Instances: int(ds.Status.NumberReady), + AppInstrumentationLabeled: appInstrumentationLabeled, + }, + }) + } + return nil + }) + if err != nil { return nil, err } - response := make([]GetApplicationItem, len(dss.Items)) - for i, ds := range dss.Items { - appInstrumentationLabeled := isObjectLabeledForInstrumentation(ds.ObjectMeta) - response[i] = GetApplicationItem{ - namespace: ds.Namespace, - nsItem: GetApplicationItemInNamespace { - Name: ds.Name, - Kind: WorkloadKindDaemonSet, - Instances: int(ds.Status.NumberReady), - AppInstrumentationLabeled: appInstrumentationLabeled, - }, - } - } - return response, nil } diff --git a/frontend/endpoints/namespaces.go b/frontend/endpoints/namespaces.go index 6edd87413..f0f6ff23d 100644 --- a/frontend/endpoints/namespaces.go +++ b/frontend/endpoints/namespaces.go @@ -5,6 +5,10 @@ import ( "fmt" "net/http" + "github.com/odigos-io/odigos/k8sutils/pkg/client" + + "k8s.io/apimachinery/pkg/runtime/schema" + "golang.org/x/sync/errgroup" "github.com/odigos-io/odigos/api/odigos/v1alpha1" @@ -69,6 +73,46 @@ func GetNamespaces(c *gin.Context, odigosns string) { c.JSON(http.StatusOK, response) } +func GetK8SNamespaces(ctx context.Context, odigosns string) GetNamespacesResponse { + + var ( + relevantNameSpaces []v1.Namespace + appsPerNamespace map[string]int + ) + + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + var err error + relevantNameSpaces, err = getRelevantNameSpaces(ctx, odigosns) + return err + }) + + g.Go(func() error { + var err error + appsPerNamespace, err = CountAppsPerNamespace(ctx) + return err + }) + + if err := g.Wait(); err != nil { + + return GetNamespacesResponse{} + } + + var response GetNamespacesResponse + for _, namespace := range relevantNameSpaces { + // check if entire namespace is instrumented + selected := namespace.Labels[consts.OdigosInstrumentationLabel] == consts.InstrumentationEnabled + + response.Namespaces = append(response.Namespaces, GetNamespaceItem{ + Name: namespace.Name, + Selected: selected, + TotalApps: appsPerNamespace[namespace.Name], + }) + } + + return response +} + // getRelevantNameSpaces returns a list of namespaces that are relevant for instrumentation. // Taking into account the ignored namespaces from the OdigosConfiguration. func getRelevantNameSpaces(ctx context.Context, odigosns string) ([]v1.Namespace, error) { @@ -177,31 +221,25 @@ func syncWorkloadsInNamespace(ctx context.Context, nsName string, workloads []Pe // returns a map, where the key is a namespace name and the value is the // number of apps in this namespace (not necessarily instrumented) func CountAppsPerNamespace(ctx context.Context) (map[string]int, error) { + namespaceToAppsCount := make(map[string]int) - deps, err := kube.DefaultClient.AppsV1().Deployments("").List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - - ss, err := kube.DefaultClient.AppsV1().StatefulSets("").List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } + resourceTypes := []string{"deployments", "statefulsets", "daemonsets"} - ds, err := kube.DefaultClient.AppsV1().DaemonSets("").List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } + for _, resourceType := range resourceTypes { + err := client.ListWithPages(client.DefaultPageSize, kube.DefaultClient.MetadataClient.Resource(schema.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: resourceType, + }).List, ctx, metav1.ListOptions{}, func(list *metav1.PartialObjectMetadataList) error { + for _, item := range list.Items { + namespaceToAppsCount[item.Namespace]++ + } + return nil + }) - namespaceToAppsCount := make(map[string]int) - for _, dep := range deps.Items { - namespaceToAppsCount[dep.Namespace]++ - } - for _, st := range ss.Items { - namespaceToAppsCount[st.Namespace]++ - } - for _, d := range ds.Items { - namespaceToAppsCount[d.Namespace]++ + if err != nil { + return nil, fmt.Errorf("failed to count %s: %w", resourceType, err) + } } return namespaceToAppsCount, nil diff --git a/frontend/graph/conversions.go b/frontend/graph/conversions.go index 49b433f2d..936727a48 100644 --- a/frontend/graph/conversions.go +++ b/frontend/graph/conversions.go @@ -90,3 +90,14 @@ func k8sSourceToGql(k8sSource *endpoints.Source) *gqlmodel.K8sActualSource { ServiceName: &k8sSource.ReportedName, } } + +func k8sApplicationItemToGql(appItem *endpoints.GetApplicationItemInNamespace) *gqlmodel.K8sActualSource { + + stringKind := string(appItem.Kind) + + return &gqlmodel.K8sActualSource{ + Kind: k8sKindToGql(stringKind), + Name: appItem.Name, + NumberOfInstances: &appItem.Instances, + } +} diff --git a/frontend/graph/schema.graphqls b/frontend/graph/schema.graphqls index 06d2ebb47..09ed84053 100644 --- a/frontend/graph/schema.graphqls +++ b/frontend/graph/schema.graphqls @@ -110,7 +110,6 @@ type ComputePlatform { id: ID! name: String computePlatformType: ComputePlatformType! - k8sActualNamespace(name: String!): K8sActualNamespace k8sActualNamespaces: [K8sActualNamespace]! k8sActualSource( diff --git a/frontend/graph/schema.resolvers.go b/frontend/graph/schema.resolvers.go index 9a8ea6532..d54ccbe98 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -39,8 +39,29 @@ func (r *queryResolver) ComputePlatform(ctx context.Context, cpID string) (*mode res[i] = k8sThinSourceToGql(&source) } + name := "odigos-system" + namespacesResponse := endpoints.GetK8SNamespaces(ctx, name) + + K8sActualNamespaces := make([]*model.K8sActualNamespace, len(namespacesResponse.Namespaces)) + + for i, namespace := range namespacesResponse.Namespaces { + namespaceActualSources := endpoints.GetApplicationsInK8SNamespace(ctx, namespace.Name) + namespaceSources := make([]*model.K8sActualSource, len(namespaceActualSources)) + for j, source := range namespaceActualSources { + namespaceSources[j] = k8sApplicationItemToGql(&source) + } + + K8sActualNamespaces[i] = &model.K8sActualNamespace{ + Name: namespace.Name, + K8sActualSources: namespaceSources, + } + } + return &model.ComputePlatform{ - K8sActualSources: res, + K8sActualSources: res, + Name: &name, + ComputePlatformType: model.ComputePlatformTypeK8s, + K8sActualNamespaces: K8sActualNamespaces, }, nil } diff --git a/frontend/kube/client.go b/frontend/kube/client.go index d1b3c8d03..1d10d81f9 100644 --- a/frontend/kube/client.go +++ b/frontend/kube/client.go @@ -5,6 +5,7 @@ import ( odigosv1alpha1 "github.com/odigos-io/odigos/api/generated/odigos/clientset/versioned/typed/odigos/v1alpha1" k8sutils "github.com/odigos-io/odigos/k8sutils/pkg/client" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/metadata" _ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) @@ -26,8 +27,9 @@ const ( type Client struct { kubernetes.Interface - OdigosClient odigosv1alpha1.OdigosV1alpha1Interface - ActionsClient actionsv1alpha1.ActionsV1alpha1Interface + OdigosClient odigosv1alpha1.OdigosV1alpha1Interface + ActionsClient actionsv1alpha1.ActionsV1alpha1Interface + MetadataClient metadata.Interface } func CreateClient(kubeConfig string) (*Client, error) { @@ -54,9 +56,15 @@ func CreateClient(kubeConfig string) (*Client, error) { return nil, err } + metadataClient, err := metadata.NewForConfig(config) + if err != nil { + return nil, err + } + return &Client{ - Interface: clientset, - OdigosClient: odigosClient, - ActionsClient: actionsClient, + Interface: clientset, + OdigosClient: odigosClient, + ActionsClient: actionsClient, + MetadataClient: metadataClient, }, nil } diff --git a/frontend/webapp/app/(setup)/choose-sources/page.tsx b/frontend/webapp/app/(setup)/choose-sources/page.tsx index 14a203ce5..495ec2f21 100644 --- a/frontend/webapp/app/(setup)/choose-sources/page.tsx +++ b/frontend/webapp/app/(setup)/choose-sources/page.tsx @@ -1,55 +1,10 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { StepsList } from '@/components'; import { ChooseSourcesContainer } from '@/containers'; import { CardWrapper, PageContainer, StepListWrapper } from '../styled'; -import { useSuspenseQuery, gql } from '@apollo/client'; - -const GET_COMPUTE_PLATFORM = gql` - query GetComputePlatform($cpId: ID!) { - computePlatform(cpId: $cpId) { - id - name - computePlatformType - k8sActualSources { - namespace - kind - name - serviceName - autoInstrumented - creationTimestamp - numberOfInstances - hasInstrumentedApplication - instrumentedApplicationDetails { - languages { - containerName - language - } - conditions { - type - status - lastTransitionTime - reason - message - } - } - } - } - } -`; - export default function ChooseSourcesPage() { - const { error, data } = useSuspenseQuery(GET_COMPUTE_PLATFORM, { - variables: { cpId: '1' }, - }); - - useEffect(() => { - if (error) { - console.error(error); - } - console.log({ data }); - }, [error, data]); return ( diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index 933dfb802..3c0fbed5a 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -1,45 +1,11 @@ 'use client'; import React from 'react'; - -import { useSuspenseQuery, gql } from '@apollo/client'; - -const GET_COMPUTE_PLATFORM = gql` - query GetComputePlatform($cpId: ID!) { - computePlatform(cpId: $cpId) { - id - name - computePlatformType - k8sActualSources { - namespace - kind - name - serviceName - autoInstrumented - creationTimestamp - numberOfInstances - hasInstrumentedApplication - instrumentedApplicationDetails { - languages { - containerName - language - } - conditions { - type - status - lastTransitionTime - reason - message - } - } - } - } - } -`; +import { ChooseSourcesContainer } from '@/containers/main'; export default function ChooseSourcesPage() { - const { error, data } = useSuspenseQuery(GET_COMPUTE_PLATFORM, { - variables: { cpId: '1' }, - }); - - return <>; + return ( + <> + + + ); } diff --git a/frontend/webapp/app/setup/layout.tsx b/frontend/webapp/app/setup/layout.tsx index f3afba447..198d6164d 100644 --- a/frontend/webapp/app/setup/layout.tsx +++ b/frontend/webapp/app/setup/layout.tsx @@ -26,11 +26,15 @@ const MainContent = styled.div` display: flex; max-width: 1440px; width: 100%; - background-color: ${({ theme }) => theme.colors.secondary}; flex-direction: column; align-items: center; `; +const ContentWrapper = styled.div` + width: 640px; + padding-top: 64px; +`; + export default function SetupLayout({ children, }: { @@ -45,9 +49,7 @@ export default function SetupLayout({ - - - {children} + {children} ); diff --git a/frontend/webapp/components/overview/actions/actions.forms/add.cluster.info/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/add.cluster.info/index.tsx index af81fd159..ac594622b 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/add.cluster.info/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/add.cluster.info/index.tsx @@ -25,6 +25,7 @@ interface ClusterAttributes { interface AddClusterInfoFormProps { data: ClusterAttributes | null; onChange: (key: string, keyValues: ClusterAttributes | null) => void; + setIsFormValid?: (value: boolean) => void; } const ACTION_DATA_KEY = 'actionData'; @@ -32,6 +33,7 @@ const ACTION_DATA_KEY = 'actionData'; export function AddClusterInfoForm({ data, onChange, + setIsFormValid = () => {}, }: AddClusterInfoFormProps): React.JSX.Element { const [keyValuePairs, setKeyValuePairs] = React.useState([]); @@ -39,6 +41,10 @@ export function AddClusterInfoForm({ buildKeyValuePairs(); }, [data]); + useEffect(() => { + validateForm(); + }, [keyValuePairs]); + function handleKeyValuesChange(keyValues: KeyValue[]): void { const actionData = { clusterAttributes: keyValues.map((keyValue) => ({ @@ -56,6 +62,8 @@ export function AddClusterInfoForm({ } else { onChange(ACTION_DATA_KEY, actionData); } + + setKeyValuePairs(keyValues); // Update state with new key-value pairs } function buildKeyValuePairs() { @@ -72,6 +80,13 @@ export function AddClusterInfoForm({ setKeyValuePairs(values || DEFAULT_KEY_VALUE_PAIR); } + function validateForm() { + const isValid = keyValuePairs.every( + (pair) => pair.key.trim() !== '' && pair.value.trim() !== '' + ); + setIsFormValid(isValid); + } + return ( <> diff --git a/frontend/webapp/components/overview/actions/actions.forms/delete.attribute/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/delete.attribute/index.tsx index 03ea03612..f05e81c47 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/delete.attribute/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/delete.attribute/index.tsx @@ -13,16 +13,33 @@ interface DeleteAttributes { interface DeleteAttributesProps { data: DeleteAttributes; onChange: (key: string, value: DeleteAttributes | null) => void; + setIsFormValid?: (value: boolean) => void; } const ACTION_DATA_KEY = 'actionData'; + export function DeleteAttributesForm({ data, onChange, + setIsFormValid = () => {}, }: DeleteAttributesProps): React.JSX.Element { + const [attributeNames, setAttributeNames] = React.useState( + data?.attributeNamesToDelete || [''] + ); + + useEffect(() => { + validateForm(); + }, [attributeNames]); + function handleOnChange(attributeNamesToDelete: string[]): void { onChange(ACTION_DATA_KEY, { attributeNamesToDelete, }); + setAttributeNames(attributeNamesToDelete); + } + + function validateForm() { + const isValid = attributeNames.every((name) => name.trim() !== ''); + setIsFormValid(isValid); } return ( @@ -32,11 +49,7 @@ export function DeleteAttributesForm({ placeholder="Add attribute names to delete" required title="Attribute Names to Delete" - values={ - data?.attributeNamesToDelete?.length > 0 - ? data.attributeNamesToDelete - : [''] - } + values={attributeNames.length > 0 ? attributeNames : ['']} onValuesChange={handleOnChange} /> diff --git a/frontend/webapp/components/overview/actions/actions.forms/dynamic.action.form/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/dynamic.action.form/index.tsx index b9c67cc13..f4d408cbc 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/dynamic.action.form/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/dynamic.action.form/index.tsx @@ -11,10 +11,10 @@ import { } from '../samplers'; import { PiiMaskingForm } from '../pii-masking'; -interface DynamicActionFormProps { - type: string | undefined; - data: any; - onChange: (key: string, value: any) => void; +interface DynamicActionFormProps { + type?: string; + data: T; + onChange: (key: string, value: T | null) => void; setIsFormValid?: (isValid: boolean) => void; } @@ -22,40 +22,31 @@ export function DynamicActionForm({ type, data, onChange, - setIsFormValid, + setIsFormValid = () => {}, }: DynamicActionFormProps): React.JSX.Element { - function renderCurrentAction() { - switch (type) { - case ActionsType.ADD_CLUSTER_INFO: - return ; - case ActionsType.DELETE_ATTRIBUTES: - return ; - case ActionsType.RENAME_ATTRIBUTES: - return ; - case ActionsType.ERROR_SAMPLER: - return ; - case ActionsType.PROBABILISTIC_SAMPLER: - return ; - case ActionsType.LATENCY_SAMPLER: - return ( - - ); - case ActionsType.PII_MASKING: - return ( - - ); - default: - return
; - } - } + const formComponents = { + [ActionsType.ADD_CLUSTER_INFO]: AddClusterInfoForm, + [ActionsType.DELETE_ATTRIBUTES]: DeleteAttributesForm, + [ActionsType.RENAME_ATTRIBUTES]: RenameAttributesForm, + [ActionsType.ERROR_SAMPLER]: ErrorSamplerForm, + [ActionsType.PROBABILISTIC_SAMPLER]: ProbabilisticSamplerForm, + [ActionsType.LATENCY_SAMPLER]: LatencySamplerForm, + [ActionsType.PII_MASKING]: PiiMaskingForm, + }; - return <>{renderCurrentAction()}; + const FormComponent = type ? formComponents[type] : null; + + return ( + <> + {FormComponent ? ( + + ) : ( +
No action form available
+ )} + + ); } diff --git a/frontend/webapp/components/overview/actions/actions.forms/rename.attributes/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/rename.attributes/index.tsx index c444b2eab..b9b62c122 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/rename.attributes/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/rename.attributes/index.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import styled from 'styled-components'; import { KeyValuePair } from '@/design.system'; import { KeyValue } from '@keyval-dev/design-system'; + const FormWrapper = styled.div` width: 375px; `; @@ -12,10 +13,12 @@ interface RenameAttributes { }; } -interface DeleteAttributesProps { +interface RenameAttributesProps { data: RenameAttributes; onChange: (key: string, value: RenameAttributes) => void; + setIsFormValid?: (value: boolean) => void; } + const DEFAULT_KEY_VALUE_PAIR = [ { id: 0, @@ -25,16 +28,22 @@ const DEFAULT_KEY_VALUE_PAIR = [ ]; const ACTION_DATA_KEY = 'actionData'; + export function RenameAttributesForm({ data, onChange, -}: DeleteAttributesProps): React.JSX.Element { + setIsFormValid = () => {}, +}: RenameAttributesProps): React.JSX.Element { const [keyValuePairs, setKeyValuePairs] = React.useState([]); useEffect(() => { buildKeyValuePairs(); }, [data]); + useEffect(() => { + validateForm(); + }, [keyValuePairs]); + function handleKeyValuesChange(keyValues: KeyValue[]): void { const renames: { [key: string]: string; @@ -44,6 +53,7 @@ export function RenameAttributesForm({ }); onChange(ACTION_DATA_KEY, { renames }); + setKeyValuePairs(keyValues); // Update state with new key-value pairs } function buildKeyValuePairs() { @@ -61,6 +71,13 @@ export function RenameAttributesForm({ setKeyValuePairs(values || DEFAULT_KEY_VALUE_PAIR); } + function validateForm() { + const isValid = keyValuePairs.every( + (pair) => pair.key.trim() !== '' && pair.value.trim() !== '' + ); + setIsFormValid(isValid); + } + return ( <> diff --git a/frontend/webapp/components/overview/actions/actions.forms/samplers/error-sampler/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/samplers/error-sampler/index.tsx index 3c71a61dd..acd15e8cf 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/samplers/error-sampler/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/samplers/error-sampler/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import { KeyvalInput } from '@/design.system'; @@ -13,18 +13,35 @@ interface ErrorSampler { interface ErrorSamplerFormProps { data: ErrorSampler; onChange: (key: string, value: ErrorSampler | null) => void; + setIsFormValid?: (value: boolean) => void; } + const ACTION_DATA_KEY = 'actionData'; + export function ErrorSamplerForm({ data, onChange, + setIsFormValid = () => {}, }: ErrorSamplerFormProps): React.JSX.Element { + useEffect(() => { + validateForm(); + }, [data?.fallback_sampling_ratio]); + function handleOnChange(fallback_sampling_ratio: number): void { onChange(ACTION_DATA_KEY, { fallback_sampling_ratio, }); } + function validateForm() { + const isValid = + !isNaN(data?.fallback_sampling_ratio) && + data?.fallback_sampling_ratio >= 0 && + data?.fallback_sampling_ratio <= 100; + + setIsFormValid(isValid); + } + return ( <> diff --git a/frontend/webapp/components/overview/actions/actions.forms/samplers/latency-action/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/samplers/latency-action/index.tsx index ce7088ad8..740605a1e 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/samplers/latency-action/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/samplers/latency-action/index.tsx @@ -74,20 +74,11 @@ export function LatencySamplerForm({ }, [filters]); const memoizedSources = React.useMemo(() => { - let instrumentsSources = sources; - if (data) { - instrumentsSources = sources.filter((source) => { - return data.endpoints_filters.every( - (filter) => filter.service_name !== source.name - ); - }); - } - - return instrumentsSources.map((source, index) => ({ + return sources?.map((source, index) => ({ id: index, label: source.name, })); - }, [sources, data]); + }, [sources]); function handleOnChange(index: number, key: string, value: any): void { const updatedFilters = filters.map((filter, i) => diff --git a/frontend/webapp/components/overview/actions/actions.forms/samplers/probabilistic-sampler/index.tsx b/frontend/webapp/components/overview/actions/actions.forms/samplers/probabilistic-sampler/index.tsx index 0a11a050a..9eb1c1f59 100644 --- a/frontend/webapp/components/overview/actions/actions.forms/samplers/probabilistic-sampler/index.tsx +++ b/frontend/webapp/components/overview/actions/actions.forms/samplers/probabilistic-sampler/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import { KeyvalInput } from '@/design.system'; @@ -13,13 +13,18 @@ interface ProbabilisticSampler { interface ProbabilisticSamplerProps { data: ProbabilisticSampler; onChange: (key: string, value: ProbabilisticSampler | null) => void; + setIsFormValid?: (value: boolean) => void; } const ACTION_DATA_KEY = 'actionData'; + export function ProbabilisticSamplerForm({ data, onChange, + setIsFormValid = () => {}, }: ProbabilisticSamplerProps): React.JSX.Element { - console.log({ data }); + useEffect(() => { + validateForm(); + }, [data?.sampling_percentage]); function handleOnChange(sampling_percentage: string): void { onChange(ACTION_DATA_KEY, { @@ -27,6 +32,12 @@ export function ProbabilisticSamplerForm({ }); } + function validateForm() { + const percentage = parseFloat(data?.sampling_percentage); + const isValid = !isNaN(percentage) && percentage >= 0 && percentage <= 100; + setIsFormValid(isValid); + } + return ( <> @@ -39,7 +50,7 @@ export function ProbabilisticSamplerForm({ min={0} max={100} error={ - +data?.sampling_percentage > 100 + parseFloat(data?.sampling_percentage) > 100 ? 'Value must be less than 100' : '' } diff --git a/frontend/webapp/components/setup/headers/header/index.tsx b/frontend/webapp/components/setup/headers/header/index.tsx index 2d6943029..b1bfa77ec 100644 --- a/frontend/webapp/components/setup/headers/header/index.tsx +++ b/frontend/webapp/components/setup/headers/header/index.tsx @@ -50,7 +50,7 @@ export const SetupHeader: React.FC = ({ onBack, onNext }) => { height={20} /> - START WITH ODIGOS + START WITH ODIGOS { )} - {step.title} + {step.title} {step.subtitle && ( - + {step.subtitle} )} diff --git a/frontend/webapp/containers/main/actions/edit-action/index.tsx b/frontend/webapp/containers/main/actions/edit-action/index.tsx index 3d4a45a2c..f6aaa0722 100644 --- a/frontend/webapp/containers/main/actions/edit-action/index.tsx +++ b/frontend/webapp/containers/main/actions/edit-action/index.tsx @@ -77,7 +77,7 @@ export function EditActionContainer(): React.JSX.Element { {ACTIONS[type].TITLE} - + diff --git a/frontend/webapp/containers/main/actions/edit-action/styled.ts b/frontend/webapp/containers/main/actions/edit-action/styled.ts index 3647a64ad..eca9d876c 100644 --- a/frontend/webapp/containers/main/actions/edit-action/styled.ts +++ b/frontend/webapp/containers/main/actions/edit-action/styled.ts @@ -25,12 +25,18 @@ export const FormFieldsWrapper = styled.div<{ disabled: boolean }>` pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; `; -export const SwitchWrapper = styled.div<{ disabled: boolean }>` +export const SwitchWrapper = styled.div<{ + disabled: boolean; + isValid: boolean; +}>` p { color: ${({ disabled }) => disabled ? theme.colors.orange_brown : theme.colors.success}; font-weight: 600; } + + opacity: ${({ isValid }) => (!isValid ? 0.3 : 1)}; + pointer-events: ${({ isValid }) => (!isValid ? 'none' : 'auto')}; `; export const KeyvalInputWrapper = styled.div` diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-list/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-list/index.tsx new file mode 100644 index 000000000..796bbe564 --- /dev/null +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-list/index.tsx @@ -0,0 +1,131 @@ +import { Text } from '@/reuseable-components'; +import Image from 'next/image'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + align-self: stretch; + border-radius: 16px; + background: ${({ theme }) => theme.colors.primary}; + height: 100%; + max-height: 548px; + overflow-y: auto; +`; + +const ListItem = styled.div<{ selected: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 16px 0px; + transition: background 0.3s; + border-radius: 16px; + + cursor: pointer; + background: ${({ selected }) => + selected ? 'rgba(68, 74, 217, 0.24)' : 'rgba(249, 249, 249, 0.04)'}; + + &:hover { + background: rgba(68, 74, 217, 0.24); + } +`; + +const ListItemContent = styled.div` + margin-left: 16px; + display: flex; + gap: 12px; +`; + +const SourceIconWrapper = styled.div` + display: flex; + width: 36px; + height: 36px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 8px; + background: linear-gradient( + 180deg, + rgba(249, 249, 249, 0.06) 0%, + rgba(249, 249, 249, 0.02) 100% + ); +`; + +const TextWrapper = styled.div` + display: flex; + flex-direction: column; + height: 36px; + justify-content: space-between; +`; + +const SelectedTextWrapper = styled.div` + margin-right: 24px; +`; + +interface K8sActualSource { + name: string; + kind: string; + numberOfInstances: number; +} + +interface SourcesListProps { + items: K8sActualSource[]; + selectedItems: K8sActualSource[]; + setSelectedItems: React.Dispatch>; +} + +const SourcesList: React.FC = ({ + items, + selectedItems, + setSelectedItems, +}) => { + const handleItemClick = (item: K8sActualSource) => { + setSelectedItems((prevSelectedItems) => + prevSelectedItems.includes(item) + ? prevSelectedItems.filter((selectedItem) => selectedItem !== item) + : [...prevSelectedItems, item] + ); + }; + + return ( + + {items.map((item) => ( + handleItemClick(item)} + > + + + source + + + {item.name} + + {item.numberOfInstances} running instances Β· {item.kind} + + + + {selectedItems.includes(item) && ( + + + SELECTED + + + )} + + ))} + + ); +}; + +export { SourcesList }; diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/index.ts b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/index.ts new file mode 100644 index 000000000..892c7ba07 --- /dev/null +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/index.ts @@ -0,0 +1,2 @@ +export * from './search-and-dropdown'; +export * from './toggles-and-checkboxes'; diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/search-and-dropdown.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/search-and-dropdown.tsx new file mode 100644 index 000000000..abf53b1be --- /dev/null +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/search-and-dropdown.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import styled from 'styled-components'; +import { SearchDropdownProps } from './type'; +import { Input, Dropdown } from '@/reuseable-components'; + +const Container = styled.div` + display: flex; + gap: 8px; + margin-top: 24px; +`; + +const SearchAndDropdown: React.FC = ({ + state, + handlers, + dropdownOptions, +}) => { + const { selectedOption, searchFilter } = state; + const { setSelectedOption, setSearchFilter } = handlers; + + return ( + + setSearchFilter(e.target.value)} + /> + + + ); +}; + +export { SearchAndDropdown }; diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/toggles-and-checkboxes.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/toggles-and-checkboxes.tsx new file mode 100644 index 000000000..5a7c604db --- /dev/null +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/toggles-and-checkboxes.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Counter, Toggle, Checkbox } from '@/reuseable-components'; +import { ToggleCheckboxHandlers, ToggleCheckboxState } from './type'; + +const Container = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const ToggleWrapper = styled.div` + display: flex; + gap: 32px; +`; + +type ToggleCheckboxProps = { + state: ToggleCheckboxState; + handlers: ToggleCheckboxHandlers; +}; + +const TogglesAndCheckboxes: React.FC = ({ + state, + handlers, +}) => { + const { + selectedAppsCount, + selectAllCheckbox, + showSelectedOnly, + futureAppsCheckbox, + } = state; + + const { setSelectAllCheckbox, setShowSelectedOnly, setFutureAppsCheckbox } = + handlers; + return ( + + + + + + + + + ); +}; + +export { TogglesAndCheckboxes }; diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/type.ts b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/type.ts new file mode 100644 index 000000000..530473dd8 --- /dev/null +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/type.ts @@ -0,0 +1,30 @@ +import { DropdownOption } from '@/types'; + +export type ToggleCheckboxState = { + selectedAppsCount: number; + selectAllCheckbox: boolean; + showSelectedOnly: boolean; + futureAppsCheckbox: boolean; +}; + +export type ToggleCheckboxHandlers = { + setSelectAllCheckbox: (value: boolean) => void; + setShowSelectedOnly: (value: boolean) => void; + setFutureAppsCheckbox: (value: boolean) => void; +}; + +export type SearchDropdownState = { + selectedOption: DropdownOption | undefined; + searchFilter: string; +}; + +export type SearchDropdownHandlers = { + setSelectedOption: (option: DropdownOption) => void; + setSearchFilter: (search: string) => void; +}; + +export type SearchDropdownProps = { + state: SearchDropdownState; + handlers: SearchDropdownHandlers; + dropdownOptions: DropdownOption[]; +}; diff --git a/frontend/webapp/containers/main/sources/choose-sources/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/index.tsx new file mode 100644 index 000000000..dec62cc78 --- /dev/null +++ b/frontend/webapp/containers/main/sources/choose-sources/index.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from 'react'; +import { useComputePlatform } from '@/hooks'; +import { SourcesList } from './choose-sources-list'; +import { SectionTitle, Divider } from '@/reuseable-components'; +import { DropdownOption, K8sActualNamespace, K8sActualSource } from '@/types'; +import { SearchAndDropdown, TogglesAndCheckboxes } from './choose-sources-menu'; +import { + SearchDropdownHandlers, + SearchDropdownState, + ToggleCheckboxHandlers, + ToggleCheckboxState, +} from './choose-sources-menu/type'; + +export function ChooseSourcesContainer() { + const [searchFilter, setSearchFilter] = useState(''); + const [showSelectedOnly, setShowSelectedOnly] = useState(false); + const [selectAllCheckbox, setSelectAllCheckbox] = useState(false); + const [futureAppsCheckbox, setFutureAppsCheckbox] = useState(false); + const [selectedOption, setSelectedOption] = useState(); + const [selectedItems, setSelectedItems] = useState([]); + const [namespacesList, setNamespacesList] = useState([]); + + const { error, data } = useComputePlatform(); + + useEffect(() => { + data && buildNamespacesList(); + }, [data, error]); + + useEffect(() => { + selectAllCheckbox ? selectAllSources() : unselectAllSources(); + }, [selectAllCheckbox]); + + function buildNamespacesList() { + const namespaces = data?.computePlatform?.k8sActualNamespaces || []; + const namespacesList = namespaces.map((namespace: K8sActualNamespace) => ({ + id: namespace.name, + value: namespace.name, + })); + + setSelectedOption(namespacesList[0]); + setNamespacesList(namespacesList); + } + + function filterSources(sources: K8sActualSource[]) { + return sources.filter((source: K8sActualSource) => { + return ( + searchFilter === '' || + source.name.toLowerCase().includes(searchFilter.toLowerCase()) + ); + }); + } + + function selectAllSources() { + const allSources = + data?.computePlatform?.k8sActualNamespaces.flatMap( + (namespace) => namespace.k8sActualSources + ) || []; + setSelectedItems(allSources); + } + + function unselectAllSources() { + setSelectedItems([]); + } + + function getVisibleSources() { + const allSources = + data?.computePlatform?.k8sActualNamespaces[0].k8sActualSources || []; + const filteredSources = searchFilter + ? filterSources(allSources) + : allSources; + + return showSelectedOnly + ? filteredSources.filter((source) => selectedItems.includes(source)) + : filteredSources; + } + + const toggleCheckboxState: ToggleCheckboxState = { + selectedAppsCount: selectedItems.length, + selectAllCheckbox, + showSelectedOnly, + futureAppsCheckbox, + }; + + const toggleCheckboxHandlers: ToggleCheckboxHandlers = { + setSelectAllCheckbox, + setShowSelectedOnly, + setFutureAppsCheckbox, + }; + + const searchDropdownState: SearchDropdownState = { + selectedOption, + searchFilter, + }; + + const searchDropdownHandlers: SearchDropdownHandlers = { + setSelectedOption, + setSearchFilter, + }; + + return ( + <> + + + + + + + + ); +} diff --git a/frontend/webapp/containers/main/sources/index.ts b/frontend/webapp/containers/main/sources/index.ts index 0a28eb6b8..c81126c2b 100644 --- a/frontend/webapp/containers/main/sources/index.ts +++ b/frontend/webapp/containers/main/sources/index.ts @@ -1,3 +1,4 @@ export * from './managed'; export * from './choose.sources'; +export * from './choose-sources'; export * from './edit.source'; diff --git a/frontend/webapp/graphql/queries/compute-platform.ts b/frontend/webapp/graphql/queries/compute-platform.ts new file mode 100644 index 000000000..6d1f3244a --- /dev/null +++ b/frontend/webapp/graphql/queries/compute-platform.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +export const GET_COMPUTE_PLATFORM = gql` + query GetComputePlatform($cpId: ID!) { + computePlatform(cpId: $cpId) { + id + name + computePlatformType + k8sActualNamespaces { + name + k8sActualSources { + name + kind + numberOfInstances + } + } + } + } +`; diff --git a/frontend/webapp/graphql/queries/index.ts b/frontend/webapp/graphql/queries/index.ts index f03c2281a..a4ba77920 100644 --- a/frontend/webapp/graphql/queries/index.ts +++ b/frontend/webapp/graphql/queries/index.ts @@ -1 +1,2 @@ export * from './config'; +export * from './compute-platform'; diff --git a/frontend/webapp/hooks/compute-platform/index.ts b/frontend/webapp/hooks/compute-platform/index.ts new file mode 100644 index 000000000..f535085ea --- /dev/null +++ b/frontend/webapp/hooks/compute-platform/index.ts @@ -0,0 +1 @@ +export * from './useComputePlatform'; diff --git a/frontend/webapp/hooks/compute-platform/useComputePlatform.ts b/frontend/webapp/hooks/compute-platform/useComputePlatform.ts new file mode 100644 index 000000000..76d9f00f6 --- /dev/null +++ b/frontend/webapp/hooks/compute-platform/useComputePlatform.ts @@ -0,0 +1,20 @@ +import { ComputePlatform } from '@/types'; +import { useQuery } from '@apollo/client'; +import { GET_COMPUTE_PLATFORM } from '@/graphql'; + +type UseComputePlatformHook = { + data?: ComputePlatform; + loading: boolean; + error?: Error; +}; + +export const useComputePlatform = (): UseComputePlatformHook => { + const { data, loading, error } = useQuery( + GET_COMPUTE_PLATFORM, + { + variables: { cpId: '1' }, + } + ); + + return { data, loading, error }; +}; diff --git a/frontend/webapp/hooks/index.tsx b/frontend/webapp/hooks/index.tsx index 7bae585c0..44c4a33e1 100644 --- a/frontend/webapp/hooks/index.tsx +++ b/frontend/webapp/hooks/index.tsx @@ -8,3 +8,4 @@ export * from './actions'; export * from './useNotify'; export * from './useSSE'; export * from './new-config'; +export * from './compute-platform'; diff --git a/frontend/webapp/public/icons/common/extend-arrow.svg b/frontend/webapp/public/icons/common/extend-arrow.svg new file mode 100644 index 000000000..9333da395 --- /dev/null +++ b/frontend/webapp/public/icons/common/extend-arrow.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/webapp/public/icons/common/folder.svg b/frontend/webapp/public/icons/common/folder.svg new file mode 100644 index 000000000..59cce3af9 --- /dev/null +++ b/frontend/webapp/public/icons/common/folder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/webapp/public/icons/common/info.svg b/frontend/webapp/public/icons/common/info.svg new file mode 100644 index 000000000..02127d33b --- /dev/null +++ b/frontend/webapp/public/icons/common/info.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/webapp/public/icons/common/search.svg b/frontend/webapp/public/icons/common/search.svg new file mode 100644 index 000000000..a278da695 --- /dev/null +++ b/frontend/webapp/public/icons/common/search.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/webapp/reuseable-components/checkbox/index.tsx b/frontend/webapp/reuseable-components/checkbox/index.tsx new file mode 100644 index 000000000..956fbb6ec --- /dev/null +++ b/frontend/webapp/reuseable-components/checkbox/index.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Tooltip } from '../tooltip'; +import Image from 'next/image'; +import { Text } from '../text'; + +interface CheckboxProps { + title: string; + tooltip?: string; + initialValue?: boolean; + onChange?: (value: boolean) => void; + disabled?: boolean; +} + +const Container = styled.div<{ disabled?: boolean }>` + display: flex; + align-items: center; + gap: 8px; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; +`; + +const CheckboxWrapper = styled.div<{ isChecked: boolean; disabled?: boolean }>` + width: 18px; + height: 18px; + border-radius: 6px; + border: 1px dashed rgba(249, 249, 249, 0.4); + display: flex; + align-items: center; + justify-content: center; + background-color: ${({ isChecked, theme }) => + isChecked ? theme.colors.primary : 'transparent'}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; +`; + +const Title = styled.span` + font-size: 16px; + color: #fff; +`; + +const Checkbox: React.FC = ({ + title, + tooltip, + initialValue = false, + onChange, + disabled, +}) => { + const [isChecked, setIsChecked] = useState(initialValue); + + const handleToggle = () => { + if (!disabled) { + const newValue = !isChecked; + setIsChecked(newValue); + if (onChange) { + onChange(newValue); + } + } + }; + + return ( + + + + {isChecked && ( + + )} + + {title} + {tooltip && ( + + )} + + + ); +}; + +export { Checkbox }; diff --git a/frontend/webapp/reuseable-components/counter/index.tsx b/frontend/webapp/reuseable-components/counter/index.tsx new file mode 100644 index 000000000..b9b242e63 --- /dev/null +++ b/frontend/webapp/reuseable-components/counter/index.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Text } from '../text'; + +interface CounterProps { + value: number; + title: string; +} + +const Container = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const ValueContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 32px; + border: 1px solid rgba(249, 249, 249, 0.24); +`; + +const Counter: React.FC = ({ value, title }) => { + return ( + + {title} + + + {value} + + + + ); +}; + +export { Counter }; diff --git a/frontend/webapp/reuseable-components/divider/index.tsx b/frontend/webapp/reuseable-components/divider/index.tsx new file mode 100644 index 000000000..5ba268e48 --- /dev/null +++ b/frontend/webapp/reuseable-components/divider/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface DividerProps { + thickness?: number; + color?: string; + margin?: string; + orientation?: 'horizontal' | 'vertical'; +} + +const StyledDivider = styled.div` + width: ${({ orientation, thickness }) => + orientation === 'vertical' ? `${thickness}px` : '100%'}; + height: ${({ orientation, thickness }) => + orientation === 'horizontal' ? `${thickness}px` : '100%'}; + background-color: ${({ color, theme }) => color || theme.colors.border}; + margin: ${({ margin }) => margin || '8px 0'}; +`; + +const Divider: React.FC = ({ + thickness = 1, + color, + margin, + orientation = 'horizontal', +}) => { + return ( + + ); +}; + +export { Divider }; diff --git a/frontend/webapp/reuseable-components/dropdown/index.tsx b/frontend/webapp/reuseable-components/dropdown/index.tsx new file mode 100644 index 000000000..b43634117 --- /dev/null +++ b/frontend/webapp/reuseable-components/dropdown/index.tsx @@ -0,0 +1,191 @@ +import React, { useState, useRef } from 'react'; +import { Input } from '../input'; +import styled, { css } from 'styled-components'; +import { Tooltip } from '../tooltip'; +import Image from 'next/image'; +import { Text } from '../text'; +import { Divider } from '../divider'; +import { DropdownOption } from '@/types'; +import { useOnClickOutside } from '@/hooks'; + +interface DropdownProps { + options: DropdownOption[]; + selectedOption: DropdownOption | undefined; + onSelect: (option: DropdownOption) => void; + title?: string; + tooltip?: string; +} + +const Container = styled.div` + display: flex; + flex-direction: column; + position: relative; + width: 100%; +`; + +const Title = styled(Text)``; + +const DropdownHeader = styled.div<{ isOpen: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + height: 36px; + padding: 0 16px; + border-radius: 32px; + border: 1px solid rgba(249, 249, 249, 0.24); + cursor: pointer; + background-color: transparent; + border-radius: 32px; + ${({ isOpen, theme }) => + isOpen && + css` + border: 1px solid rgba(249, 249, 249, 0.48); + background: rgba(249, 249, 249, 0.08); + `}; + + &:hover { + border-color: ${({ theme }) => theme.colors.secondary}; + } + &:focus-within { + border-color: ${({ theme }) => theme.colors.secondary}; + } +`; + +const DropdownListContainer = styled.div` + position: absolute; + top: 60px; + left: 0; + width: 100%; + max-height: 200px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + background-color: #242424; + border: 1px solid ${({ theme }) => theme.colors.border}; + border-radius: 32px; + margin-top: 4px; + z-index: 999; +`; + +const SearchInputContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const DropdownItem = styled.div<{ isSelected: boolean }>` + padding: 8px 12px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 32px; + &:hover { + background: rgba(68, 74, 217, 0.24); + } + ${({ isSelected, theme }) => + isSelected && + css` + background: rgba(68, 74, 217, 0.24); + `} +`; + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +`; + +const OpenDropdownIcon = styled(Image)<{ isOpen: boolean }>` + transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')}; +`; + +const Dropdown: React.FC = ({ + options, + selectedOption, + onSelect, + title, + tooltip, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const dropdownRef = useRef(null); + + useOnClickOutside(dropdownRef, () => setIsOpen(false)); + + const filteredOptions = options.filter((option) => + option.value.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleSelect = (option: DropdownOption) => { + onSelect(option); + setIsOpen(false); + }; + + return ( + + {title && ( + + + {title} + {tooltip && ( + + )} + + + )} + setIsOpen(!isOpen)}> + {selectedOption?.value} + + + + {isOpen && ( + + + setSearchTerm(e.target.value)} + /> + + + {filteredOptions.map((option) => ( + handleSelect(option)} + > + {option.value} + + {option.id === selectedOption?.id && ( + + )} + + ))} + + )} + + ); +}; + +export { Dropdown }; diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index c912adb8d..4090df8fb 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -1,2 +1,10 @@ export * from './text'; export * from './button'; +export * from './section-title'; +export * from './input'; +export * from './tooltip'; +export * from './dropdown'; +export * from './divider'; +export * from './counter'; +export * from './toggle'; +export * from './checkbox'; diff --git a/frontend/webapp/reuseable-components/input/index.tsx b/frontend/webapp/reuseable-components/input/index.tsx new file mode 100644 index 000000000..9b62c8bc1 --- /dev/null +++ b/frontend/webapp/reuseable-components/input/index.tsx @@ -0,0 +1,193 @@ +import Image from 'next/image'; +import React from 'react'; +import styled, { css } from 'styled-components'; +import { Text } from '../text'; +import { Tooltip } from '../tooltip'; + +interface InputProps extends React.InputHTMLAttributes { + icon?: string; + buttonLabel?: string; + onButtonClick?: () => void; + errorMessage?: string; + title?: string; + tooltip?: string; +} + +const Container = styled.div` + display: flex; + flex-direction: column; + position: relative; + width: 100%; +`; + +const InputWrapper = styled.div<{ + isDisabled?: boolean; + hasError?: boolean; + isActive?: boolean; +}>` + width: 100%; + display: flex; + align-items: center; + height: 36px; + gap: 12px; + + transition: border-color 0.3s; + border-radius: 32px; + border: 1px solid rgba(249, 249, 249, 0.24); + ${({ isDisabled }) => + isDisabled && + css` + background-color: #555; + cursor: not-allowed; + opacity: 0.6; + `} + + ${({ hasError }) => + hasError && + css` + border-color: red; + `} + + ${({ isActive }) => + isActive && + css` + border-color: ${({ theme }) => theme.colors.secondary}; + `} + + &:hover { + border-color: ${({ theme }) => theme.colors.secondary}; + } + &:focus-within { + border-color: ${({ theme }) => theme.colors.secondary}; + } +`; + +const StyledInput = styled.input` + flex: 1; + border: none; + outline: none; + background: none; + color: ${({ theme }) => theme.colors.text}; + font-size: 14px; + + &::placeholder { + color: ${({ theme }) => theme.colors.text}; + font-family: ${({ theme }) => theme.font_family.primary}; + opacity: 0.4; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; /* 157.143% */ + } + + &:disabled { + background-color: #555; + cursor: not-allowed; + } +`; + +const IconWrapper = styled.div` + display: flex; + align-items: center; + margin-left: 12px; +`; + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.primary}; + border: none; + color: #fff; + padding: 8px 16px; + border-radius: 20px; + cursor: pointer; + margin-left: 8px; + + &:hover { + background-color: ${({ theme }) => theme.colors.secondary}; + } + + &:disabled { + background-color: #555; + cursor: not-allowed; + } +`; + +const ErrorWrapper = styled.div` + position: relative; +`; + +const ErrorMessage = styled(Text)` + color: red; + font-size: 12px; + position: absolute; + top: 100%; + left: 0; + margin-top: 4px; +`; + +const Title = styled(Text)` + font-size: 16px; + font-weight: bold; + margin-bottom: 4px; +`; + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +`; + +const Input: React.FC = ({ + icon, + buttonLabel, + onButtonClick, + errorMessage, + title, + tooltip, + ...props +}) => { + return ( + + {title && ( + + + {title} + {tooltip && ( + + )} + + + )} + + + {icon && ( + + + + )} + + {buttonLabel && onButtonClick && ( + + )} + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}; + +export { Input }; diff --git a/frontend/webapp/reuseable-components/section-title/index.tsx b/frontend/webapp/reuseable-components/section-title/index.tsx new file mode 100644 index 000000000..01b9d2e08 --- /dev/null +++ b/frontend/webapp/reuseable-components/section-title/index.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Text } from '../text'; +import { Button } from '../button'; +import styled from 'styled-components'; + +interface SectionTitleProps { + title: string; + description: string; + buttonText?: string; + onButtonClick?: () => void; +} + +const Container = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +`; + +const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +const Title = styled(Text)``; + +const Description = styled(Text)``; + +const ActionButton = styled(Button)``; + +const SectionTitle: React.FC = ({ + title, + description, + buttonText, + onButtonClick, +}) => { + return ( + + + + {title} + + + {description} + + + {buttonText && onButtonClick && ( + + {buttonText} + + )} + + ); +}; + +export { SectionTitle }; diff --git a/frontend/webapp/reuseable-components/text/index.tsx b/frontend/webapp/reuseable-components/text/index.tsx index 75fbc78b6..a375ed19a 100644 --- a/frontend/webapp/reuseable-components/text/index.tsx +++ b/frontend/webapp/reuseable-components/text/index.tsx @@ -8,20 +8,22 @@ interface TextProps { weight?: number; align?: 'left' | 'center' | 'right'; family?: 'primary' | 'secondary'; + opacity?: number; } -const TextWrapper = styled.span<{ +const TextWrapper = styled.div<{ color?: string; size: number; weight: number; align: 'left' | 'center' | 'right'; family?: 'primary' | 'secondary'; + opacity: number; }>` - color: ${({ color, theme }) => - color || console.log({ theme }) || theme.colors.text}; + color: ${({ color, theme }) => color || theme.colors.text}; font-size: ${({ size }) => size}px; font-weight: ${({ weight }) => weight}; text-align: ${({ align }) => align}; + opacity: ${({ opacity }) => opacity}; font-family: ${({ theme, family }) => { if (family === 'primary') { return theme.font_family.primary; @@ -37,9 +39,10 @@ const Text: React.FC = ({ children, color, size = 16, - weight = 400, + weight = 300, align = 'left', family = 'primary', + opacity = 1, }) => { return ( = ({ size={size} weight={weight} align={align} + opacity={opacity} > {children} diff --git a/frontend/webapp/reuseable-components/toggle/index.tsx b/frontend/webapp/reuseable-components/toggle/index.tsx new file mode 100644 index 000000000..e3377c4a0 --- /dev/null +++ b/frontend/webapp/reuseable-components/toggle/index.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import styled, { css } from 'styled-components'; +import { Tooltip } from '../tooltip'; +import { Text } from '../text'; + +interface ToggleProps { + title: string; + tooltip?: string; + initialValue?: boolean; + onChange?: (value: boolean) => void; + disabled?: boolean; +} + +const Container = styled.div<{ disabled?: boolean }>` + display: flex; + align-items: center; + gap: 12px; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; +`; + +const ToggleSwitch = styled.div<{ isActive: boolean; disabled?: boolean }>` + width: 24px; + height: 12px; + border: 1px dashed #aaa; + border-radius: 20px; + display: flex; + align-items: center; + padding: 2px; + background-color: ${({ isActive, theme }) => + isActive ? theme.colors.primary : 'transparent'}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + opacity: ${({ isActive }) => (isActive ? 1 : 0.4)}; + &::before { + content: ''; + width: 12px; + height: 12px; + border-radius: 50%; + background-color: ${({ theme }) => theme.colors.secondary}; + transform: ${({ isActive }) => + isActive ? 'translateX(12px)' : 'translateX(0)'}; + transition: transform 0.3s; + } +`; + +const Toggle: React.FC = ({ + title, + tooltip, + initialValue = false, + onChange, + disabled, +}) => { + const [isActive, setIsActive] = useState(initialValue); + + const handleToggle = () => { + if (!disabled) { + const newValue = !isActive; + setIsActive(newValue); + if (onChange) { + onChange(newValue); + } + } + }; + + return ( + + + + {title} + {tooltip && ( + + )} + + + ); +}; + +export { Toggle }; diff --git a/frontend/webapp/reuseable-components/tooltip/index.tsx b/frontend/webapp/reuseable-components/tooltip/index.tsx new file mode 100644 index 000000000..111e8bcb7 --- /dev/null +++ b/frontend/webapp/reuseable-components/tooltip/index.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Text } from '../text'; + +interface TooltipProps { + text: string; + children: React.ReactNode; +} + +const TooltipContainer = styled.div` + position: relative; + display: inline-block; + width: fit-content; + cursor: pointer; +`; + +const TooltipText = styled.div` + visibility: hidden; + background-color: ${({ theme }) => theme.colors.dark_grey}; + background: #1a1a1a; + color: #fff; + text-align: center; + border-radius: 4px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; /* Position the tooltip above the text */ + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + opacity: 0; + transition: opacity 0.3s; + + /* Tooltip arrow */ + &::after { + content: ''; + position: absolute; + z-index: 99999; + top: 100%; /* At the bottom of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #1a1a1a transparent transparent transparent; + } +`; + +const TooltipWrapper = styled.div<{ hasText: boolean }>` + &:hover ${TooltipText} { + ${({ hasText }) => + hasText && + ` + visibility: visible; + opacity: 1; + z-index: 999; + `} + } +`; + +const Tooltip: React.FC = ({ text, children }) => { + const hasText = !!text; + + return ( + + + {children} + {hasText && ( + + {text} + + )} + + + ); +}; + +export { Tooltip }; diff --git a/frontend/webapp/styles/theme.ts b/frontend/webapp/styles/theme.ts index 6789e96cf..ce5ce7c11 100644 --- a/frontend/webapp/styles/theme.ts +++ b/frontend/webapp/styles/theme.ts @@ -6,6 +6,7 @@ const colors = { secondary: '#F9F9F9', dark_grey: '#151515', text: '#F9F9F9', + border: 'rgba(249, 249, 249, 0.08)', }; const text = { @@ -18,8 +19,8 @@ const text = { }; const font_family = { - primary: 'Kode Mono, sans-serif', - secondary: 'Inter, sans-serif', + primary: 'Inter, sans-serif', + secondary: 'Kode Mono, sans-serif', }; // Define the theme interface diff --git a/frontend/webapp/types/common.ts b/frontend/webapp/types/common.ts index 2640ccccf..3d57d580a 100644 --- a/frontend/webapp/types/common.ts +++ b/frontend/webapp/types/common.ts @@ -23,3 +23,8 @@ export type Config = { installation: string; }; }; + +export interface DropdownOption { + id: string; + value: string; +} diff --git a/frontend/webapp/types/compute-platform.ts b/frontend/webapp/types/compute-platform.ts new file mode 100644 index 000000000..e2233dadc --- /dev/null +++ b/frontend/webapp/types/compute-platform.ts @@ -0,0 +1,16 @@ +import { K8sActualSource } from './sources'; +export type K8sActualNamespace = { + name: string; + k8sActualSources: K8sActualSource[]; +}; + +type ComputePlatformData = { + id: string; + name: string; + computePlatformType: string; + k8sActualNamespaces: K8sActualNamespace[]; +}; + +export type ComputePlatform = { + computePlatform: ComputePlatformData; +}; diff --git a/frontend/webapp/types/index.ts b/frontend/webapp/types/index.ts index 18bec3378..1f91e3d5b 100644 --- a/frontend/webapp/types/index.ts +++ b/frontend/webapp/types/index.ts @@ -3,3 +3,4 @@ export * from './destinations'; export * from './actions'; export * from './sources'; export * from './common'; +export * from './compute-platform'; diff --git a/frontend/webapp/types/sources.ts b/frontend/webapp/types/sources.ts index 43eeb90dc..f7d8e9709 100644 --- a/frontend/webapp/types/sources.ts +++ b/frontend/webapp/types/sources.ts @@ -70,3 +70,9 @@ export interface SelectedSources { future_selected: boolean; }; } + +export type K8sActualSource = { + name: string; + kind: string; + numberOfInstances: number; +}; diff --git a/helm/odigos/Chart.yaml b/helm/odigos/Chart.yaml index 574dc984e..d3f93b350 100644 --- a/helm/odigos/Chart.yaml +++ b/helm/odigos/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: odigos -description: Odigos Helm Chart for Kubernetes +description: Odigos distribution for Kubernetes type: application -# v0.0.0 will be replaced by the git tag version on release -version: "v0.0.0" -appVersion: "v0.0.0" +# 0.0.0 will be replaced by the git tag version on release +version: "0.0.0" +appVersion: "0.0.0" icon: https://d2q89wckrml3k4.cloudfront.net/logo.png diff --git a/k8sutils/pkg/client/pager.go b/k8sutils/pkg/client/pager.go new file mode 100644 index 000000000..5561788d2 --- /dev/null +++ b/k8sutils/pkg/client/pager.go @@ -0,0 +1,30 @@ +package client + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const DefaultPageSize = 500 + +type listFunc[T metav1.ListInterface] func(context.Context, metav1.ListOptions) (T, error) + +func ListWithPages[T metav1.ListInterface](pageSize int, list listFunc[T], ctx context.Context, opts metav1.ListOptions, handler func(obj T) error) error { + opts.Limit = int64(pageSize) + opts.Continue = "" + for { + obj, err := list(ctx, opts) + if err != nil { + return err + } + if err := handler(obj); err != nil { + return err + } + if obj.GetContinue() == "" { + break + } + opts.Continue = obj.GetContinue() + } + return nil +} diff --git a/k8sutils/pkg/instrumentation_instance/status.go b/k8sutils/pkg/instrumentation_instance/status.go index 03aa75594..dff55f46d 100644 --- a/k8sutils/pkg/instrumentation_instance/status.go +++ b/k8sutils/pkg/instrumentation_instance/status.go @@ -90,7 +90,7 @@ func InstrumentationInstanceName(owner client.Object, pid int) string { return fmt.Sprintf("%s-%d", owner.GetName(), pid) } -func PersistInstrumentationInstanceStatus(ctx context.Context, owner client.Object, kubeClient client.Client, instrumentedAppName string, pid int, scheme *runtime.Scheme, options ...InstrumentationInstanceOption) error { +func PersistInstrumentationInstanceStatus(ctx context.Context, owner client.Object, containerName string, kubeClient client.Client, instrumentedAppName string, pid int, scheme *runtime.Scheme, options ...InstrumentationInstanceOption) error { instrumentationInstanceName := InstrumentationInstanceName(owner, pid) updatedInstance := &odigosv1.InstrumentationInstance{ TypeMeta: metav1.TypeMeta{ @@ -103,7 +103,11 @@ func PersistInstrumentationInstanceStatus(ctx context.Context, owner client.Obje Labels: map[string]string{ consts.InstrumentedAppNameLabel: instrumentedAppName, }, - }} + }, + Spec: odigosv1.InstrumentationInstanceSpec{ + ContainerName: containerName, + }, + } err := controllerutil.SetControllerReference(owner, updatedInstance, scheme) if err != nil { diff --git a/odiglet/Dockerfile b/odiglet/Dockerfile index a5497a5c9..c9bf6ece8 100644 --- a/odiglet/Dockerfile +++ b/odiglet/Dockerfile @@ -58,7 +58,7 @@ ARG DOTNET_OTEL_VERSION=v0.7.0 ADD https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$DOTNET_OTEL_VERSION/opentelemetry-dotnet-instrumentation-linux-musl.zip . RUN unzip opentelemetry-dotnet-instrumentation-linux-musl.zip && rm opentelemetry-dotnet-instrumentation-linux-musl.zip -FROM --platform=$BUILDPLATFORM keyval/odiglet-base:v1.4 as builder +FROM --platform=$BUILDPLATFORM keyval/odiglet-base:v1.4 AS builder WORKDIR /go/src/github.com/odigos-io/odigos # Copy local modules required by the build COPY api/ api/ diff --git a/odiglet/go.mod b/odiglet/go.mod index d6fa2beff..7ab7a28c2 100644 --- a/odiglet/go.mod +++ b/odiglet/go.mod @@ -14,15 +14,15 @@ require ( github.com/odigos-io/odigos/opampserver v0.0.0 github.com/odigos-io/odigos/procdiscovery v0.0.0 github.com/odigos-io/opentelemetry-zap-bridge v0.0.5 - go.opentelemetry.io/auto v0.13.0-alpha.0.20240705154812-28b663b26905 - go.opentelemetry.io/otel v1.27.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 + go.opentelemetry.io/auto v0.14.0-alpha + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.65.0 - k8s.io/api v0.30.2 + k8s.io/api v0.30.3 k8s.io/apimachinery v0.30.3 - k8s.io/client-go v0.30.2 - k8s.io/kubelet v0.30.2 + k8s.io/client-go v0.30.3 + k8s.io/kubelet v0.30.3 sigs.k8s.io/controller-runtime v0.18.4 ) @@ -63,36 +63,40 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.53.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.52.0 // indirect - go.opentelemetry.io/contrib/exporters/autoexport v0.52.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.49.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/sdk v1.27.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - go.opentelemetry.io/proto/otlp v1.2.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.53.0 // indirect + go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.50.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 // indirect + go.opentelemetry.io/otel/log v0.4.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.4.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/oauth2 v0.20.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/odiglet/go.sum b/odiglet/go.sum index 71a8867c4..6b52b66a2 100644 --- a/odiglet/go.sum +++ b/odiglet/go.sum @@ -276,13 +276,13 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= -github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= @@ -311,42 +311,48 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/auto v0.13.0-alpha.0.20240705154812-28b663b26905 h1:jkn+mjs7Cpfzj/bjs8M9Ums2bxoABgq9ZtFI4aeL9M4= -go.opentelemetry.io/auto v0.13.0-alpha.0.20240705154812-28b663b26905/go.mod h1:7lDId8pdd0bm8odWRLh8xdA01d7oRtLMxwLCZAuibtc= -go.opentelemetry.io/collector/pdata v1.7.0 h1:/WNsBbE6KM3TTPUb9v/5B7IDqnDkgf8GyFhVJJqu7II= -go.opentelemetry.io/collector/pdata v1.7.0/go.mod h1:ehCBBA5GoFrMZkwyZAKGY/lAVSgZf6rzUt3p9mddmPU= -go.opentelemetry.io/contrib/bridges/prometheus v0.52.0 h1:NNkEjNcUXeNcxDTNLyyAmFHefByhj8YU1AojgcPqbfs= -go.opentelemetry.io/contrib/bridges/prometheus v0.52.0/go.mod h1:Dv7d2yUvusfblvi9qMQby+youF09GiUVWRWkdogrDtE= -go.opentelemetry.io/contrib/exporters/autoexport v0.52.0 h1:G/AGl5O78ZKHs63Rl65P1HyZfDnTyxjv8r7dbdZ9fB0= -go.opentelemetry.io/contrib/exporters/autoexport v0.52.0/go.mod h1:WoVWPZjJ7EB5Z9aROW1DZuRIoFEemxmhCdZJlcjY2AE= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0/go.mod h1:TNupZ6cxqyFEpLXAZW7On+mLFL0/g0TE3unIYL91xWc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= -go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.opentelemetry.io/auto v0.14.0-alpha h1:Dc8MoRawqVu/0EQxTpDjBmUHHRSQ6ATbKkjGoUTWaOw= +go.opentelemetry.io/auto v0.14.0-alpha/go.mod h1:OyuWH1KVgewc6YKakfDn48arE3giVB/IyQRtH/47oPI= +go.opentelemetry.io/contrib/bridges/prometheus v0.53.0 h1:BdkKDtcrHThgjcEia1737OUuFdP6xzBKAMx2sNZCkvE= +go.opentelemetry.io/contrib/bridges/prometheus v0.53.0/go.mod h1:ZkhVxcJgeXlL/lVyT/vxNHVFiSG5qOaDwYaSgD8IfZo= +go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 h1:13K+tY7E8GJInkrvRiPAhC0gi/7vKjzDNhtmCf+QXG8= +go.opentelemetry.io/contrib/exporters/autoexport v0.53.0/go.mod h1:lyQF6xQ4iDnMg4sccNdFs1zf62xd79YI8vZqKjOTwMs= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 h1:zBPZAISA9NOc5cE8zydqDiS0itvg/P/0Hn9m72a5gvM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0/go.mod h1:gcj2fFjEsqpV3fXuzAA+0Ze1p2/4MJ4T7d77AmkvueQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0 h1:0MH3f8lZrflbUWXVxyBg/zviDFdGE062uKh5+fu8Vv0= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0/go.mod h1:Vh68vYiHY5mPdekTr0ox0sALsqjoVy0w3Os278yX5SQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 h1:BJee2iLkfRfl9lc7aFmBwkWxY/RI1RDdXepSF6y8TPE= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0/go.mod h1:DIzlHs3DRscCIBU3Y9YSzPfScwnYnzfnCd4g8zA7bZc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y= +go.opentelemetry.io/otel/log v0.4.0 h1:/vZ+3Utqh18e8TPjuc3ecg284078KWrR8BRz+PQAj3o= +go.opentelemetry.io/otel/log v0.4.0/go.mod h1:DhGnQvky7pHy82MIRV43iXh3FlKN8UUKftn0KbLOq6I= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/log v0.4.0 h1:1mMI22L82zLqf6KtkjrRy5BbagOTWdJsqMY/HSqILAA= +go.opentelemetry.io/otel/sdk/log v0.4.0/go.mod h1:AYJ9FVF0hNOgAVzUG/ybg/QttnXhUePWAupmCqtdESo= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -365,8 +371,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -410,14 +416,14 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -453,16 +459,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -491,8 +497,8 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -522,10 +528,10 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200916143405-f6a2fa72f0c4/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -579,16 +585,16 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.19.2/go.mod h1:IQpK0zFQ1xc5iNIQPqzgoOwuFugaYHK4iCknlAQP9nI= -k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= -k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA= -k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= -k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= +k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= k8s.io/component-base v0.19.2/go.mod h1:g5LrsiTiabMLZ40AR6Hl45f088DevyGY+cCE2agEIVo= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= @@ -599,8 +605,8 @@ k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kubelet v0.19.2/go.mod h1:FHHoByVWzh6kNaarXaDPAa751Oz6REcOVRyFT84L1Is= -k8s.io/kubelet v0.30.2 h1:Ck4E/pHndI20IzDXxS57dElhDGASPO5pzXF7BcKfmCY= -k8s.io/kubelet v0.30.2/go.mod h1:DSwwTbLQmdNkebAU7ypIALR4P9aXZNFwgRmedojUE94= +k8s.io/kubelet v0.30.3 h1:KvGWDdhzD0vEyDyGTCjsDc8D+0+lwRMw3fJbfQgF7ys= +k8s.io/kubelet v0.30.3/go.mod h1:D9or45Vkzcqg55CEiqZ8dVbwP3Ksj7DruEVRS9oq3Ys= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/odiglet/pkg/ebpf/director.go b/odiglet/pkg/ebpf/director.go index c1400b94a..5caf168db 100644 --- a/odiglet/pkg/ebpf/director.go +++ b/odiglet/pkg/ebpf/director.go @@ -51,12 +51,13 @@ const ( ) type instrumentationStatus struct { - Workload common.PodWorkload - PodName types.NamespacedName - Healthy bool - Message string - Reason InstrumentationStatusReason - Pid int + Workload common.PodWorkload + PodName types.NamespacedName + ContainerName string + Healthy bool + Message string + Reason InstrumentationStatusReason + Pid int } type EbpfDirector[T OtelEbpfSdk] struct { @@ -140,7 +141,7 @@ func (d *EbpfDirector[T]) observeInstrumentations(ctx context.Context, scheme *r } instrumentedAppName := workload.GetRuntimeObjectName(status.Workload.Name, status.Workload.Kind) - err = inst.PersistInstrumentationInstanceStatus(ctx, &pod, d.client, instrumentedAppName, status.Pid, scheme, + err = inst.PersistInstrumentationInstanceStatus(ctx, &pod, status.ContainerName, d.client, instrumentedAppName, status.Pid, scheme, inst.WithHealthy(&status.Healthy), inst.WithMessage(status.Message), inst.WithReason(string(status.Reason)), @@ -188,12 +189,13 @@ func (d *EbpfDirector[T]) Instrument(ctx context.Context, pid int, pod types.Nam return case <-loadedIndicator: d.instrumentationStatusChan <- instrumentationStatus{ - Healthy: true, - Message: "Successfully loaded eBPF probes to pod: " + pod.String(), - Workload: *podWorkload, - Reason: LoadedSuccessfully, - PodName: pod, - Pid: pid, + Healthy: true, + Message: "Successfully loaded eBPF probes to pod: " + pod.String(), + Workload: *podWorkload, + Reason: LoadedSuccessfully, + PodName: pod, + ContainerName: containerName, + Pid: pid, } } }() @@ -204,12 +206,13 @@ func (d *EbpfDirector[T]) Instrument(ctx context.Context, pid int, pod types.Nam inst, err := d.instrumentationFactory.CreateEbpfInstrumentation(ctx, pid, appName, podWorkload, containerName, pod.Name, loadedIndicator) if err != nil { d.instrumentationStatusChan <- instrumentationStatus{ - Healthy: false, - Message: err.Error(), - Workload: *podWorkload, - Reason: FailedToInitialize, - PodName: pod, - Pid: pid, + Healthy: false, + Message: err.Error(), + Workload: *podWorkload, + Reason: FailedToInitialize, + PodName: pod, + ContainerName: containerName, + Pid: pid, } return } @@ -234,12 +237,13 @@ func (d *EbpfDirector[T]) Instrument(ctx context.Context, pid int, pod types.Nam if err := inst.Run(context.Background()); err != nil { d.instrumentationStatusChan <- instrumentationStatus{ - Healthy: false, - Message: err.Error(), - Workload: *podWorkload, - Reason: FailedToLoad, - PodName: pod, - Pid: pid, + Healthy: false, + Message: err.Error(), + Workload: *podWorkload, + Reason: FailedToLoad, + PodName: pod, + ContainerName: containerName, + Pid: pid, } } }() diff --git a/opampserver/pkg/connection/conncache.go b/opampserver/pkg/connection/conncache.go index fc92b8011..fd0b39705 100644 --- a/opampserver/pkg/connection/conncache.go +++ b/opampserver/pkg/connection/conncache.go @@ -31,13 +31,13 @@ func NewConnectionsCache() *ConnectionsCache { } } -// GetConnection returns the connection information for the given device id. +// GetConnection returns the connection information for the given OpAMP instanceUid. // the returned object is a by-value copy of the connection information, so it can be safely used. // To change something in the connection information, use the functions below which are synced and safe. -func (c *ConnectionsCache) GetConnection(deviceId string) (*ConnectionInfo, bool) { +func (c *ConnectionsCache) GetConnection(instanceUid string) (*ConnectionInfo, bool) { c.mux.Lock() defer c.mux.Unlock() - conn, ok := c.liveConnections[deviceId] + conn, ok := c.liveConnections[instanceUid] if !ok || conn == nil { return nil, false } else { @@ -47,30 +47,30 @@ func (c *ConnectionsCache) GetConnection(deviceId string) (*ConnectionInfo, bool } } -func (c *ConnectionsCache) AddConnection(deviceId string, conn *ConnectionInfo) { +func (c *ConnectionsCache) AddConnection(instanceUid string, conn *ConnectionInfo) { // copy the conn object to avoid it being accessed concurrently connCopy := *conn c.mux.Lock() defer c.mux.Unlock() - c.liveConnections[deviceId] = &connCopy + c.liveConnections[instanceUid] = &connCopy } -func (c *ConnectionsCache) RemoveConnection(deviceId string) { +func (c *ConnectionsCache) RemoveConnection(instanceUid string) { c.mux.Lock() defer c.mux.Unlock() - delete(c.liveConnections, deviceId) + delete(c.liveConnections, instanceUid) } -func (c *ConnectionsCache) RecordMessageTime(deviceId string) { +func (c *ConnectionsCache) RecordMessageTime(instanceUid string) { c.mux.Lock() defer c.mux.Unlock() - conn, ok := c.liveConnections[deviceId] + conn, ok := c.liveConnections[instanceUid] if !ok { return } conn.lastMessageTime = time.Now() - c.liveConnections[deviceId] = conn + c.liveConnections[instanceUid] = conn } func (c *ConnectionsCache) CleanupStaleConnections() []ConnectionInfo { diff --git a/opampserver/pkg/connection/types.go b/opampserver/pkg/connection/types.go index f5ceb274a..3f96b0aa3 100644 --- a/opampserver/pkg/connection/types.go +++ b/opampserver/pkg/connection/types.go @@ -13,6 +13,7 @@ type ConnectionInfo struct { DeviceId string Workload common.PodWorkload Pod *corev1.Pod + ContainerName string Pid int64 InstrumentedAppName string lastMessageTime time.Time diff --git a/opampserver/pkg/server/handlers.go b/opampserver/pkg/server/handlers.go index 18b6e2b41..f3753a0d6 100644 --- a/opampserver/pkg/server/handlers.go +++ b/opampserver/pkg/server/handlers.go @@ -86,6 +86,7 @@ func (c *ConnectionHandlers) OnNewConnection(ctx context.Context, deviceId strin DeviceId: deviceId, Workload: podWorkload, Pod: pod, + ContainerName: k8sAttributes.ContainerName, Pid: pid, InstrumentedAppName: instrumentedAppName, AgentRemoteConfig: fullRemoteConfig, @@ -147,7 +148,7 @@ func (c *ConnectionHandlers) PersistInstrumentationDeviceStatus(ctx context.Cont } healthy := true // TODO: populate this field with real health status - err := instrumentation_instance.PersistInstrumentationInstanceStatus(ctx, connectionInfo.Pod, c.kubeclient, connectionInfo.InstrumentedAppName, int(connectionInfo.Pid), c.scheme, + err := instrumentation_instance.PersistInstrumentationInstanceStatus(ctx, connectionInfo.Pod, connectionInfo.ContainerName, c.kubeclient, connectionInfo.InstrumentedAppName, int(connectionInfo.Pid), c.scheme, instrumentation_instance.WithIdentifyingAttributes(identifyingAttributes), instrumentation_instance.WithMessage("Agent connected"), instrumentation_instance.WithHealthy(&healthy), diff --git a/opampserver/pkg/server/server.go b/opampserver/pkg/server/server.go index 86bc9ab3f..281908477 100644 --- a/opampserver/pkg/server/server.go +++ b/opampserver/pkg/server/server.go @@ -65,6 +65,13 @@ func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager, return } + instanceUid := string(agentToServer.InstanceUid) + if instanceUid == "" { + logger.Error(err, "InstanceUid is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + deviceId := req.Header.Get("X-Odigos-DeviceId") if deviceId == "" { logger.Error(err, "X-Odigos-DeviceId header is missing") @@ -73,7 +80,7 @@ func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager, } var serverToAgent *protobufs.ServerToAgent - connectionInfo, exists := connectionCache.GetConnection(deviceId) + connectionInfo, exists := connectionCache.GetConnection(instanceUid) if !exists { connectionInfo, serverToAgent, err = handlers.OnNewConnection(ctx, deviceId, &agentToServer) if err != nil { @@ -82,13 +89,13 @@ func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager, return } if connectionInfo != nil { - connectionCache.AddConnection(deviceId, connectionInfo) + connectionCache.AddConnection(instanceUid, connectionInfo) } } else { if agentToServer.AgentDisconnect != nil { handlers.OnConnectionClosed(ctx, connectionInfo) - connectionCache.RemoveConnection(deviceId) + connectionCache.RemoveConnection(instanceUid) } serverToAgent, err = handlers.OnAgentToServerMessage(ctx, &agentToServer, connectionInfo) @@ -113,7 +120,7 @@ func StartOpAmpServer(ctx context.Context, logger logr.Logger, mgr ctrl.Manager, } // keep record in memory of last message time, to detect stale connections - connectionCache.RecordMessageTime(deviceId) + connectionCache.RecordMessageTime(instanceUid) serverToAgent.InstanceUid = agentToServer.InstanceUid diff --git a/scripts/release-charts.sh b/scripts/release-charts.sh old mode 100644 new mode 100755 index 443a4824b..04ece9bd8 --- a/scripts/release-charts.sh +++ b/scripts/release-charts.sh @@ -1,8 +1,17 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash # Setup TMPDIR="$(mktemp -d)" -CHARTDIR="helm/odigos" +CHARTDIRS=("helm/odigos") + +prefix () { + echo "${@:1}" + echo "${@:2}" + for i in "${@:2}"; do + echo "Renaming $i to $1$i" + mv "$i" "$1$i" + done +} if [ -z "$TAG" ]; then echo "TAG required" @@ -14,7 +23,7 @@ if [ -z "$GITHUB_REPOSITORY" ]; then exit 1 fi -if [[ $(git diff -- $CHARTDIR | wc -c) -ne 0 ]]; then +if [[ $(git diff -- ${CHARTDIRS[*]} | wc -c) -ne 0 ]]; then echo "Helm chart dirty. Aborting." exit 1 fi @@ -24,16 +33,21 @@ helm repo add odigos https://odigos-io.github.io/odigos-charts 2> /dev/null || t git worktree add $TMPDIR gh-pages -f # Update index with new packages -sed -i -E 's/v0.0.0/'"${TAG}"'/' $CHARTDIR/Chart.yaml -helm package helm/* -d $TMPDIR +for chart in "${CHARTDIRS[@]}" +do + echo "Updating $chart/Chart.yaml with version ${TAG#v}" + sed -i -E 's/0.0.0/'"${TAG#v}"'/' $chart/Chart.yaml +done +helm package ${CHARTDIRS[*]} -d $TMPDIR pushd $TMPDIR +prefix 'test-helm-assets-' *.tgz helm repo index . --merge index.yaml --url https://github.com/$GITHUB_REPOSITORY/releases/download/$TAG/ +git diff -G apiVersion # The check avoids pushing the same tag twice and only pushes if there's a new entry in the index if [[ $(git diff -G apiVersion | wc -c) -ne 0 ]]; then # Upload new packages - rename 'odigos' 'test-helm-assets-odigos' *.tgz - gh release upload -R $GITHUB_REPOSITORY $TAG $TMPDIR/*.tgz + gh release upload -R $GITHUB_REPOSITORY $TAG $TMPDIR/*.tgz || exit 1 git add index.yaml git commit -m "update index with $TAG" && git push @@ -45,5 +59,5 @@ else fi # Roll back chart version changes -git checkout $CHARTDIR +git checkout ${CHARTDIRS[*]} git worktree remove $TMPDIR -f || echo " -> Failed to clean up temp worktree" diff --git a/tests/common/flush_traces.sh b/tests/common/flush_traces.sh new file mode 100755 index 000000000..20b539d6f --- /dev/null +++ b/tests/common/flush_traces.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Ensure the script fails if any command fails +set -e + +function flush_traces() { + local dest_namespace="traces" + local dest_service="e2e-tests-tempo" + local dest_port="tempo-prom-metrics" + kubectl get --raw /api/v1/namespaces/$dest_namespace/services/$dest_service:$dest_port/proxy/flush + # check if command succeeded + if [ $? -eq 0 ]; then + echo "Traces flushed successfully" + else + echo "Failed to flush traces" + exit 1 + fi +} + +flush_traces \ No newline at end of file diff --git a/tests/common/traceql_runner.sh b/tests/common/traceql_runner.sh new file mode 100755 index 000000000..b36bd8e3c --- /dev/null +++ b/tests/common/traceql_runner.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Ensure the script fails if any command fails +set -e + +# Function to verify the YAML schema +function verify_yaml_schema() { + local file=$1 + local query=$(yq e '.query' "$file") + local expected_count=$(yq e '.expected.count' "$file") + + if [ -z "$query" ] || [ "$expected_count" == "null" ] || [ -z "$expected_count" ]; then + echo "Invalid YAML schema in file: $file" + exit 1 + fi +} + +function urlencode() ( + local length="${#1}" + for (( i = 0; i < length; i++ )); do + local c="${1:i:1}" + case $c in + [a-zA-Z0-9.~_-]) printf "$c" ;; + *) printf '%%%02X' "'$c" ;; + esac + done +) + +# Function to process a YAML file +function process_yaml_file() { + local dest_namespace="traces" + local dest_service="e2e-tests-tempo" + local dest_port="tempo-prom-metrics" + + local file=$1 + file_name=$(basename "$file") + echo "Running test $file_name" + query=$(yq '.query' "$file") + encoded_query=$(urlencode "$query") + expected_count=$(yq e '.expected.count' "$file") + current_epoch=$(date +%s) + one_hour=3600 + start_epoch=$(($current_epoch - one_hour)) + end_epoch=$(($current_epoch + one_hour)) + response=$(kubectl get --raw /api/v1/namespaces/$dest_namespace/services/$dest_service:$dest_port/proxy/api/search\?end=$end_epoch\&start=$start_epoch\&q=$encoded_query) + num_of_traces=$(echo $response | jq '.traces | length') + # if num_of_traces not equal to expected_count + if [ "$num_of_traces" -ne "$expected_count" ]; then + echo "Test FAILED: expected $expected_count got $num_of_traces" + echo "$response" | jq + exit 1 + else + echo "Test PASSED" + exit 0 + fi +} + +# Check if the first argument is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Test file path +TEST_FILE=$1 + +# Check if yq is installed +if ! command -v yq &> /dev/null; then + echo "yq command not found. Please install yq." + exit 1 +fi + +verify_yaml_schema $TEST_FILE +process_yaml_file $TEST_FILE diff --git a/tests/common/wait_for_dest.sh b/tests/common/wait_for_dest.sh new file mode 100755 index 000000000..6179dffe6 --- /dev/null +++ b/tests/common/wait_for_dest.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Ensure the script fails if any command fails +set -e + +# function to verify tempo is ready +# This is needed due to bug in tempo - It reports Ready before it is actually ready +# So we manually hit the health check endpoint to verify it is ready +function wait_for_ready() { + local dest_namespace="traces" + local dest_service="e2e-tests-tempo" + local dest_port="tempo-prom-metrics" + local response=$(kubectl get --raw /api/v1/namespaces/$dest_namespace/services/$dest_service:$dest_port/proxy/ready) + if [ "$response" != "ready" ]; then + echo "Tempo is not ready yet. Retrying in 2 seconds..." + sleep 2 + wait_for_ready + else + echo "Tempo is ready" + sleep 2 + fi +} + +wait_for_ready \ No newline at end of file diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 000000000..7a00951d4 --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,101 @@ +# Odigos End to End Testing +In addition to unit tests, Odigos has a suite of end-to-end tests that are run on every pull request. +These tests are installing multiple microservices, instrument with Odigos, generate traffic, and validate the results. + +## Tools +- [Kubernetes In Docker (KinD)](https://kind.sigs.k8s.io/) - a tool for running local Kubernetes clusters using Docker container β€œnodes”. +- [Chainsaw](https://kyverno.github.io/chainsaw/) - To orchestrate the different Kubernetes actions. +- [Tempo](https://github.com/grafana/tempo) - Distributed tracing backend. Chosen due to its query language that allows for easy querying of traces. + +## Running e2e locally +To run the end-to-end tests you need to have the following: +- kubectl configured to a fresh Kubernetes cluster. For local development, you can use KinD but also managed clusters like EKS should work. +- yq and jq installed. You can install it via: +```bash +brew install yq +brew install jq +``` +- Odigos cli compiled at `cli` folder. Compile via: +```bash +go build -tags=embed_manifests -o ./cli/odigos ./cli +``` +- Odigos images tagged with `e2e-test` preloaded to the cluster. If you are using KinD you can run: +```bash +TAG=e2e-test make build-images load-to-kind +``` +- Chainsaw binary, installed via one of the following methods: + - Hombrew: + ```bash + brew tap kyverno/chainsaw https://github.com/kyverno/chainsaw + brew install kyverno/chainsaw/chainsaw + ``` + - Go: + ```bash + go install github.com/kyverno/chainsaw@latest + ``` + +To run specific scenarios, for example `multi-apps` run from Odigos root directory: +```bash +chainsaw test tests/e2e/multi-apps +``` + +## Writing new scenarios +Every scenario should include some/all of the following: +- Install destination (usually Tempo) +- Install test applications +- Install Odigos +- Select apps for instrumentation and configure destination +- Generate traffic +- Validate traces + +Scenarios are written in yaml files called `chainsaw-test.yaml` according to the Chainsaw schema. + +See the [following document](https://kyverno.github.io/chainsaw/latest/test/) for more information on how to write scenarios. + +Scenarios should be placed in the `tests/e2e/` directory and TraceQL validations should be placed in the `tests/e2e//traceql` directory. + +After writing and testing new scenario, you should also add it to the GitHub Action file location at: +`.github/workflows/e2e.yaml` to run it on every pull request. + +## Working with TraceQL +TraceQL is a query language that allows you to query traces in Tempo. +It is used in the end-to-end tests to validate the traces generated by Odigos. + +### Connecting to Tempo +In order to run TraceQL queries, you need to connect to Tempo. +Tempo is installed automatically in the e2e test, so if you ran a scenario you can connect to it. +You can do this by port-forwarding the Tempo service: +```bash +kubectl port-forward svc/e2e-tests-tempo 3100:3100 -n traces +``` + +### Querying traces +Then you can execute TraceQL queries via: +```bash +curl -G -s http://localhost:3100/api/search --data-urlencode 'q={ resource.odigos.version = "e2e-test"}' +``` + +To get full individual trace you can use the following command: +```bash +curl -G -s http://localhost:3100/api/traces/3debdffae5920741a53d1bd015c62b29 +``` + +For both APIs it is recommended to pipe the results to `jq` for better readability and `less` for paging. + +### Writing queries +See [the following document](https://grafana.com/docs/tempo/latest/traceql/) for more information on how to write queries. +In order to add new traceql test, you need to add a new yaml file in the following schema: +```yaml +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: +query: | + +expected: + count: +``` + +Once you have the file, you can run the test via: +```bash +tests/e2e/common/traceql_runner.sh +``` \ No newline at end of file diff --git a/.github/workflows/e2e/kv-shop.yaml b/tests/e2e/helm-chart/02-install-simple-demo.yaml similarity index 70% rename from .github/workflows/e2e/kv-shop.yaml rename to tests/e2e/helm-chart/02-install-simple-demo.yaml index dc16d78ee..d12e8abd4 100644 --- a/.github/workflows/e2e/kv-shop.yaml +++ b/tests/e2e/helm-chart/02-install-simple-demo.yaml @@ -1,31 +1,37 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: membership + name: coupon + namespace: default labels: - app: membership + app: coupon spec: selector: matchLabels: - app: membership + app: coupon template: metadata: labels: - app: membership + app: coupon spec: containers: - - name: membership - image: keyval/kv-shop-membership:v0.2 + - name: coupon + image: keyval/odigos-demo-coupon:v0.1 + imagePullPolicy: IfNotPresent + env: + - name: MEMBERSHIP_SERVICE_HOST + value: "membership:8080" ports: - containerPort: 8080 --- kind: Service apiVersion: v1 metadata: - name: membership + name: coupon + namespace: default spec: selector: - app: membership + app: coupon ports: - protocol: TCP port: 8080 @@ -34,45 +40,55 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - name: coupon + name: frontend + namespace: default labels: - app: coupon - odigos-instrumentation: disabled + app: frontend spec: selector: matchLabels: - app: coupon + app: frontend template: metadata: labels: - app: coupon + app: frontend spec: containers: - - name: coupon - image: keyval/kv-shop-coupon:v0.2 + - name: frontend + image: keyval/odigos-demo-frontend:v0.2 + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 1000 env: - - name: NODE_IP - valueFrom: - fieldRef: - fieldPath: status.hostIP - - name: OTEL_TRACES_EXPORTER - value: otlp - - name: OTEL_EXPORTER_OTLP_ENDPOINT - value: "http://$(NODE_IP):4318" - - name: OTEL_SERVICE_NAME - value: coupon - - name: MEMBERSHIP_SERVICE_URL - value: "membership:8080" + - name: INVENTORY_SERVICE_HOST + value: inventory:8080 + - name: PRICING_SERVICE_HOST + value: pricing:8080 + - name: COUPON_SERVICE_HOST + value: coupon:8080 ports: - containerPort: 8080 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 --- kind: Service apiVersion: v1 metadata: - name: coupon + name: frontend + namespace: default spec: selector: - app: coupon + app: frontend ports: - protocol: TCP port: 8080 @@ -82,6 +98,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: inventory + namespace: default labels: app: inventory spec: @@ -95,7 +112,8 @@ spec: spec: containers: - name: inventory - image: keyval/kv-shop-inventory:v0.2 + image: keyval/odigos-demo-inventory:v0.1 + imagePullPolicy: IfNotPresent ports: - containerPort: 8080 --- @@ -103,6 +121,7 @@ kind: Service apiVersion: v1 metadata: name: inventory + namespace: default spec: selector: app: inventory @@ -114,31 +133,34 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - name: pricing + name: membership + namespace: default labels: - app: pricing + app: membership spec: selector: matchLabels: - app: pricing + app: membership template: metadata: labels: - app: pricing + app: membership spec: containers: - - name: pricing - image: keyval/kv-shop-pricing:v0.2 + - name: membership + image: keyval/odigos-demo-membership:v0.1 + imagePullPolicy: IfNotPresent ports: - containerPort: 8080 --- kind: Service apiVersion: v1 metadata: - name: pricing + name: membership + namespace: default spec: selector: - app: pricing + app: membership ports: - protocol: TCP port: 8080 @@ -147,38 +169,34 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - name: frontend + name: pricing + namespace: default labels: - app: frontend + app: pricing spec: selector: matchLabels: - app: frontend + app: pricing template: metadata: labels: - app: frontend + app: pricing spec: containers: - - name: frontend - image: keyval/kv-shop-frontend:v0.2 - env: - - name: INVENTORY_SERVICE_HOST - value: inventory:8080 - - name: PRICING_SERVICE_HOST - value: pricing:8080 - - name: COUPON_SERVICE_HOST - value: coupon:8080 + - name: pricing + image: keyval/odigos-demo-pricing:v0.1 + imagePullPolicy: IfNotPresent ports: - containerPort: 8080 --- kind: Service apiVersion: v1 metadata: - name: frontend + name: pricing + namespace: default spec: selector: - app: frontend + app: pricing ports: - protocol: TCP port: 8080 diff --git a/tests/e2e/helm-chart/03-instrument-ns.yaml b/tests/e2e/helm-chart/03-instrument-ns.yaml new file mode 100644 index 000000000..6814c325f --- /dev/null +++ b/tests/e2e/helm-chart/03-instrument-ns.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: default + labels: + odigos-instrumentation: enabled \ No newline at end of file diff --git a/tests/e2e/helm-chart/04-add-destination.yaml b/tests/e2e/helm-chart/04-add-destination.yaml new file mode 100644 index 000000000..7a637f3f6 --- /dev/null +++ b/tests/e2e/helm-chart/04-add-destination.yaml @@ -0,0 +1,12 @@ +apiVersion: odigos.io/v1alpha1 +kind: Destination +metadata: + name: odigos.io.dest.tempo-123123 + namespace: odigos-test-ns +spec: + data: + TEMPO_URL: e2e-tests-tempo.traces:4317 + destinationName: e2e-tests + signals: + - TRACES + type: tempo \ No newline at end of file diff --git a/.github/workflows/e2e/buybot-job.yaml b/tests/e2e/helm-chart/05-generate-traffic.yaml similarity index 77% rename from .github/workflows/e2e/buybot-job.yaml rename to tests/e2e/helm-chart/05-generate-traffic.yaml index 6120a7709..fb94d0f53 100644 --- a/.github/workflows/e2e/buybot-job.yaml +++ b/tests/e2e/helm-chart/05-generate-traffic.yaml @@ -2,6 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: name: buybot-job + namespace: default spec: template: metadata: @@ -18,5 +19,5 @@ spec: - name: curl image: curlimages/curl:8.4.0 imagePullPolicy: IfNotPresent - command: ["curl"] - args: ["-s","-X","POST","http://frontend:8080/buy?id=123"] + command: [ "curl" ] + args: [ "-s","-X","POST","http://frontend:8080/buy?id=123" ] diff --git a/tests/e2e/helm-chart/assert-apps-installed.yaml b/tests/e2e/helm-chart/assert-apps-installed.yaml new file mode 100644 index 000000000..c78756927 --- /dev/null +++ b/tests/e2e/helm-chart/assert-apps-installed.yaml @@ -0,0 +1,69 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app: frontend + namespace: default +status: + containerStatuses: + - name: frontend + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: coupon + namespace: default +status: + containerStatuses: + - name: coupon + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: inventory + namespace: default +status: + containerStatuses: + - name: inventory + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: membership + namespace: default +status: + containerStatuses: + - name: membership + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: pricing + namespace: default +status: + containerStatuses: + - name: pricing + ready: true + restartCount: 0 + started: true + phase: Running \ No newline at end of file diff --git a/tests/e2e/helm-chart/assert-instrumented-and-pipeline.yaml b/tests/e2e/helm-chart/assert-instrumented-and-pipeline.yaml new file mode 100644 index 000000000..6265089de --- /dev/null +++ b/tests/e2e/helm-chart/assert-instrumented-and-pipeline.yaml @@ -0,0 +1,319 @@ +apiVersion: odigos.io/v1alpha1 +kind: CollectorsGroup +metadata: + name: odigos-data-collection + namespace: odigos-test-ns +spec: + role: NODE_COLLECTOR +status: + ready: true +--- +apiVersion: odigos.io/v1alpha1 +kind: CollectorsGroup +metadata: + name: odigos-gateway + namespace: odigos-test-ns +spec: + role: CLUSTER_GATEWAY +status: + ready: true +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + odigos.io/collector: "true" + name: odigos-gateway + namespace: odigos-test-ns + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-gateway +spec: + replicas: 1 + selector: + matchLabels: + odigos.io/collector: "true" + template: + metadata: + labels: + odigos.io/collector: "true" + spec: + containers: + - env: + - name: ODIGOS_VERSION + valueFrom: + configMapKeyRef: + key: ODIGOS_VERSION + name: odigos-deployment + - name: GOMEMLIMIT + (value != null): true + name: gateway + resources: + requests: + (memory != null): true + volumeMounts: + - mountPath: /conf + name: collector-conf + volumes: + - configMap: + defaultMode: 420 + items: + - key: collector-conf + path: collector-conf.yaml + name: odigos-gateway + name: collector-conf +status: + availableReplicas: 1 + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: odigos-gateway + namespace: odigos-test-ns + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-gateway +(data != null): true +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: odigos-data-collection + namespace: odigos-test-ns + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-data-collection +(data != null): true +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + odigos.io/data-collection: "true" + name: odigos-data-collection + namespace: odigos-test-ns + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-data-collection +spec: + selector: + matchLabels: + odigos.io/data-collection: "true" + template: + metadata: + labels: + odigos.io/data-collection: "true" + spec: + containers: + - name: data-collection + securityContext: + privileged: true + volumeMounts: + - mountPath: /conf + name: conf + - mountPath: /var/lib/docker/containers + name: varlibdockercontainers + readOnly: true + - mountPath: /var/log + name: varlog + readOnly: true + - mountPath: /var/lib/kubelet/pod-resources + name: kubeletpodresources + readOnly: true + hostNetwork: true + nodeSelector: + kubernetes.io/os: linux + securityContext: {} + serviceAccount: odigos-data-collection + serviceAccountName: odigos-data-collection + volumes: + - configMap: + defaultMode: 420 + items: + - key: conf + path: conf.yaml + name: odigos-data-collection + name: conf + - hostPath: + path: /var/log + type: "" + name: varlog + - hostPath: + path: /var/lib/docker/containers + type: "" + name: varlibdockercontainers + - hostPath: + path: /var/lib/kubelet/pod-resources + type: "" + name: kubeletpodresources +status: + numberAvailable: 1 + numberReady: 1 +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: frontend +spec: + containers: + - name: frontend + resources: + limits: + instrumentation.odigos.io/java-native-community: "1" + requests: + instrumentation.odigos.io/java-native-community: "1" +status: + containerStatuses: + - name: frontend + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: coupon +spec: + containers: + - name: coupon + resources: + limits: + instrumentation.odigos.io/javascript-native-community: "1" + requests: + instrumentation.odigos.io/javascript-native-community: "1" +status: + containerStatuses: + - name: coupon + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: inventory +spec: + containers: + - name: inventory + resources: + limits: + instrumentation.odigos.io/python-native-community: "1" + requests: + instrumentation.odigos.io/python-native-community: "1" +status: + containerStatuses: + - name: inventory + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: membership +spec: + containers: + - name: membership + resources: + limits: + instrumentation.odigos.io/go-ebpf-community: "1" + requests: + instrumentation.odigos.io/go-ebpf-community: "1" +status: + containerStatuses: + - name: membership + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: pricing +spec: + containers: + - name: pricing + resources: + limits: + instrumentation.odigos.io/dotnet-native-community: "1" + requests: + instrumentation.odigos.io/dotnet-native-community: "1" +status: + containerStatuses: + - name: pricing + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentationInstance +metadata: + namespace: default + labels: + instrumented-app: deployment-coupon +status: + healthy: true + identifyingAttributes: + - key: service.instance.id + (value != null): true + - key: telemetry.sdk.language + value: nodejs + - key: telemetry.distro.version + value: e2e-test + - key: process.pid + (value != null): true +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentationInstance +metadata: + namespace: default + labels: + instrumented-app: deployment-inventory +status: + healthy: true + identifyingAttributes: + - key: service.instance.id + (value != null): true + - key: process.pid + (value != null): true + - key: telemetry.sdk.language + value: python +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentationInstance +metadata: + namespace: default + labels: + instrumented-app: deployment-membership +status: + healthy: true + reason: LoadedSuccessfully \ No newline at end of file diff --git a/tests/e2e/helm-chart/assert-odigos-installed.yaml b/tests/e2e/helm-chart/assert-odigos-installed.yaml new file mode 100644 index 000000000..09c944c44 --- /dev/null +++ b/tests/e2e/helm-chart/assert-odigos-installed.yaml @@ -0,0 +1,114 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: odigos-test-ns + labels: + odigos.io/system-object: "true" +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odigos-autoscaler + namespace: odigos-test-ns +status: + containerStatuses: + - name: manager + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odigos-scheduler + namespace: odigos-test-ns +status: + containerStatuses: + - name: manager + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odigos-instrumentor + namespace: odigos-test-ns +status: + containerStatuses: + - name: manager + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odiglet + namespace: odigos-test-ns + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: DaemonSet + name: odiglet +spec: + containers: + - name: odiglet + resources: {} + securityContext: + capabilities: + add: + - SYS_PTRACE + privileged: true + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + serviceAccount: odiglet + serviceAccountName: odiglet +status: + containerStatuses: + - name: odiglet + ready: true + restartCount: 0 + started: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: destinations.odigos.io +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: instrumentedapplications.odigos.io +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: odigosconfigurations.odigos.io +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: processors.odigos.io \ No newline at end of file diff --git a/tests/e2e/helm-chart/assert-runtime-detected.yaml b/tests/e2e/helm-chart/assert-runtime-detected.yaml new file mode 100644 index 000000000..f0894f78a --- /dev/null +++ b/tests/e2e/helm-chart/assert-runtime-detected.yaml @@ -0,0 +1,79 @@ +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-coupon + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: coupon +spec: + runtimeDetails: + - containerName: coupon + language: javascript +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-frontend + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: frontend +spec: + runtimeDetails: + - containerName: frontend + language: java +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-inventory + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: inventory +spec: + runtimeDetails: + - containerName: inventory + language: python +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-membership + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: membership +spec: + runtimeDetails: + - containerName: membership + language: go +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-pricing + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: pricing +spec: + runtimeDetails: + - containerName: pricing + language: dotnet diff --git a/tests/e2e/helm-chart/assert-tempo-running.yaml b/tests/e2e/helm-chart/assert-tempo-running.yaml new file mode 100644 index 000000000..f4653f4a3 --- /dev/null +++ b/tests/e2e/helm-chart/assert-tempo-running.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: e2e-tests-tempo-0 + namespace: traces +status: + phase: Running + containerStatuses: + - name: tempo + ready: true + restartCount: 0 \ No newline at end of file diff --git a/tests/e2e/helm-chart/assert-traffic-job-running.yaml b/tests/e2e/helm-chart/assert-traffic-job-running.yaml new file mode 100644 index 000000000..0557b2742 --- /dev/null +++ b/tests/e2e/helm-chart/assert-traffic-job-running.yaml @@ -0,0 +1,10 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: buybot-job + namespace: default +status: + conditions: + - status: "True" + type: Complete + succeeded: 1 diff --git a/tests/e2e/helm-chart/chainsaw-test.yaml b/tests/e2e/helm-chart/chainsaw-test.yaml new file mode 100644 index 000000000..f90d4b402 --- /dev/null +++ b/tests/e2e/helm-chart/chainsaw-test.yaml @@ -0,0 +1,130 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: helm-chart +spec: + description: This e2e test install Odigos via helm chart on custom namespace + skipDelete: true + steps: + - name: Prepare destination + try: + - script: + timeout: 60s + content: | + helm repo add grafana https://grafana.github.io/helm-charts + helm repo update + helm install e2e-tests grafana/tempo -n traces --create-namespace --set tempo.storage.trace.block.version=vParquet4 --version 1.10.1 + - assert: + file: assert-tempo-running.yaml + - name: Wait for destination to be ready + try: + - script: + timeout: 60s + content: ../../common/wait_for_dest.sh + - name: Install Odigos + try: + - script: + content: | + git clone https://github.com/odigos-io/odigos-charts.git /tmp/odigos-charts + # Retry to avoid flakiness (due to CRD race conditions in helm). Timeout after 60s. + while ! helm upgrade --install odigos /tmp/odigos-charts/charts/odigos --create-namespace --namespace odigos-test-ns --set image.tag=e2e-test; do + echo "Failed to install Odigos, retrying..." + sleep 1 + done + kubectl label namespace odigos-test-ns odigos.io/system-object="true" + rm -rf /tmp/odigos-charts + timeout: 60s + - name: Verify Odigos Installation + try: + - script: + content: | + export ACTUAL_VERSION=$(../../../cli/odigos version --cluster) + if [ "$ACTUAL_VERSION" != "e2e-test" ]; then + echo "Odigos version is not e2e-test, got $ACTUAL_VERSION" + exit 1 + fi + - assert: + file: assert-odigos-installed.yaml + - name: Install Demo App + try: + - script: + timeout: 100s + content: | + docker pull keyval/odigos-demo-inventory:v0.1 + docker pull keyval/odigos-demo-membership:v0.1 + docker pull keyval/odigos-demo-coupon:v0.1 + docker pull keyval/odigos-demo-inventory:v0.1 + docker pull keyval/odigos-demo-frontend:v0.2 + kind load docker-image keyval/odigos-demo-inventory:v0.1 + kind load docker-image keyval/odigos-demo-membership:v0.1 + kind load docker-image keyval/odigos-demo-coupon:v0.1 + kind load docker-image keyval/odigos-demo-inventory:v0.1 + kind load docker-image keyval/odigos-demo-frontend:v0.2 + - apply: + file: 02-install-simple-demo.yaml + - assert: + file: assert-apps-installed.yaml + - name: Detect Languages + try: + - apply: + file: 03-instrument-ns.yaml + - assert: + file: assert-runtime-detected.yaml + - name: Add Destination + try: + - apply: + file: 04-add-destination.yaml + - assert: + file: assert-instrumented-and-pipeline.yaml + - name: Generate Traffic + try: + - script: + timeout: 60s + content: | + while true; do + # Apply the job + kubectl apply -f 05-generate-traffic.yaml + + # Wait for the job to complete + job_name=$(kubectl get -f 05-generate-traffic.yaml -o=jsonpath='{.metadata.name}') + kubectl wait --for=condition=complete job/$job_name + + # Delete the job + kubectl delete -f 05-generate-traffic.yaml + + # Run the wait-for-trace script + ../../common/traceql_runner.sh tracesql/wait-for-trace.yaml + if [ $? -eq 0 ]; then + break + else + sleep 3 + ../../common/flush_traces.sh + fi + done + - name: Verify Trace - Context Propagation + try: + - script: + content: | + ../../common/traceql_runner.sh tracesql/context-propagation.yaml + catch: + - podLogs: + name: odiglet + namespace: odigos-system + - name: Verify Trace - Resource Attributes + try: + - script: + content: | + ../../common/traceql_runner.sh tracesql/resource-attributes.yaml + catch: + - podLogs: + name: odiglet + namespace: odigos-system + - name: Verify Trace - Span Attributes + try: + - script: + content: | + ../../common/traceql_runner.sh tracesql/span-attributes.yaml + catch: + - podLogs: + name: odiglet + namespace: odigos-system diff --git a/tests/e2e/helm-chart/tracesql/context-propagation.yaml b/tests/e2e/helm-chart/tracesql/context-propagation.yaml new file mode 100644 index 000000000..9c463f9b3 --- /dev/null +++ b/tests/e2e/helm-chart/tracesql/context-propagation.yaml @@ -0,0 +1,13 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: This test checks if the context propagation is working correctly between different languages +query: | + { resource.service.name = "frontend" && resource.telemetry.sdk.language = "java" && + span.http.request.method = "POST" && span.http.route = "/buy" && span:kind = server } + >> ( + { resource.service.name = "pricing" && resource.telemetry.sdk.language = "dotnet" } && + { resource.service.name = "inventory" && resource.telemetry.sdk.language = "python" } && + ({ resource.service.name = "coupon" && resource.telemetry.sdk.language = "nodejs" } + >> { resource.service.name = "membership" && resource.telemetry.sdk.language = "go" })) +expected: + count: 1 \ No newline at end of file diff --git a/tests/e2e/helm-chart/tracesql/resource-attributes.yaml b/tests/e2e/helm-chart/tracesql/resource-attributes.yaml new file mode 100644 index 000000000..934439b7e --- /dev/null +++ b/tests/e2e/helm-chart/tracesql/resource-attributes.yaml @@ -0,0 +1,14 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: | + This test check the following resource attributes: + A. odigos.version attribute exists on all spans and has the correct value + B. Kubernetes attributes are correctly set on all spans + At the time of writing this test, TraceQL api does not support not equal to nil so we use regex instead. +query: | + { resource.odigos.version != "e2e-test" || + resource.k8s.deployment.name !~ ".*" || + resource.k8s.node.name !~ "kind-control-plane" || + resource.k8s.pod.name !~ ".*" } +expected: + count: 0 \ No newline at end of file diff --git a/tests/e2e/helm-chart/tracesql/span-attributes.yaml b/tests/e2e/helm-chart/tracesql/span-attributes.yaml new file mode 100644 index 000000000..d508d4a39 --- /dev/null +++ b/tests/e2e/helm-chart/tracesql/span-attributes.yaml @@ -0,0 +1,18 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: | + This test checks the span attributes for a specific trace. + TODO - JS, Python and DotNet SDK are not generating data in latest semconv. add additional checks when they are updated. +query: | + { resource.service.name = "frontend" && resource.telemetry.sdk.language = "java" && + span.http.request.method = "POST" && span.http.route = "/buy" && span:kind = server && + span.http.response.status_code = 200 && span.url.query = "id=123" } + >> ( + { resource.service.name = "pricing" && resource.telemetry.sdk.language = "dotnet" && span:kind = server } && + { resource.service.name = "inventory" && resource.telemetry.sdk.language = "python" && span:kind = server } && + ({ resource.service.name = "coupon" && resource.telemetry.sdk.language = "nodejs" && span:kind = server } + >> { resource.service.name = "membership" && resource.telemetry.sdk.language = "go" && + span.http.request.method = "GET" && span:kind = server && + span.http.response.status_code = 200 && span.url.path = "/isMember" })) +expected: + count: 1 \ No newline at end of file diff --git a/tests/e2e/helm-chart/tracesql/wait-for-trace.yaml b/tests/e2e/helm-chart/tracesql/wait-for-trace.yaml new file mode 100644 index 000000000..a88f58987 --- /dev/null +++ b/tests/e2e/helm-chart/tracesql/wait-for-trace.yaml @@ -0,0 +1,11 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: This test waits for a trace that goes from frontend to pricing, inventory, coupon, and membership services +query: | + { resource.service.name = "frontend" } && + { resource.service.name = "pricing" } && + { resource.service.name = "inventory" } && + { resource.service.name = "coupon" } && + { resource.service.name = "membership" } +expected: + count: 1 \ No newline at end of file diff --git a/tests/e2e/multi-apps/02-install-simple-demo.yaml b/tests/e2e/multi-apps/02-install-simple-demo.yaml new file mode 100644 index 000000000..d12e8abd4 --- /dev/null +++ b/tests/e2e/multi-apps/02-install-simple-demo.yaml @@ -0,0 +1,203 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coupon + namespace: default + labels: + app: coupon +spec: + selector: + matchLabels: + app: coupon + template: + metadata: + labels: + app: coupon + spec: + containers: + - name: coupon + image: keyval/odigos-demo-coupon:v0.1 + imagePullPolicy: IfNotPresent + env: + - name: MEMBERSHIP_SERVICE_HOST + value: "membership:8080" + ports: + - containerPort: 8080 +--- +kind: Service +apiVersion: v1 +metadata: + name: coupon + namespace: default +spec: + selector: + app: coupon + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: default + labels: + app: frontend +spec: + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: keyval/odigos-demo-frontend:v0.2 + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 1000 + env: + - name: INVENTORY_SERVICE_HOST + value: inventory:8080 + - name: PRICING_SERVICE_HOST + value: pricing:8080 + - name: COUPON_SERVICE_HOST + value: coupon:8080 + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +kind: Service +apiVersion: v1 +metadata: + name: frontend + namespace: default +spec: + selector: + app: frontend + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: inventory + namespace: default + labels: + app: inventory +spec: + selector: + matchLabels: + app: inventory + template: + metadata: + labels: + app: inventory + spec: + containers: + - name: inventory + image: keyval/odigos-demo-inventory:v0.1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 +--- +kind: Service +apiVersion: v1 +metadata: + name: inventory + namespace: default +spec: + selector: + app: inventory + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: membership + namespace: default + labels: + app: membership +spec: + selector: + matchLabels: + app: membership + template: + metadata: + labels: + app: membership + spec: + containers: + - name: membership + image: keyval/odigos-demo-membership:v0.1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 +--- +kind: Service +apiVersion: v1 +metadata: + name: membership + namespace: default +spec: + selector: + app: membership + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pricing + namespace: default + labels: + app: pricing +spec: + selector: + matchLabels: + app: pricing + template: + metadata: + labels: + app: pricing + spec: + containers: + - name: pricing + image: keyval/odigos-demo-pricing:v0.1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 +--- +kind: Service +apiVersion: v1 +metadata: + name: pricing + namespace: default +spec: + selector: + app: pricing + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 \ No newline at end of file diff --git a/tests/e2e/multi-apps/03-instrument-ns.yaml b/tests/e2e/multi-apps/03-instrument-ns.yaml new file mode 100644 index 000000000..6814c325f --- /dev/null +++ b/tests/e2e/multi-apps/03-instrument-ns.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: default + labels: + odigos-instrumentation: enabled \ No newline at end of file diff --git a/tests/e2e/multi-apps/04-add-destination.yaml b/tests/e2e/multi-apps/04-add-destination.yaml new file mode 100644 index 000000000..a847f9e44 --- /dev/null +++ b/tests/e2e/multi-apps/04-add-destination.yaml @@ -0,0 +1,12 @@ +apiVersion: odigos.io/v1alpha1 +kind: Destination +metadata: + name: odigos.io.dest.tempo-123123 + namespace: odigos-system +spec: + data: + TEMPO_URL: e2e-tests-tempo.traces:4317 + destinationName: e2e-tests + signals: + - TRACES + type: tempo \ No newline at end of file diff --git a/tests/e2e/multi-apps/05-generate-traffic.yaml b/tests/e2e/multi-apps/05-generate-traffic.yaml new file mode 100644 index 000000000..fb94d0f53 --- /dev/null +++ b/tests/e2e/multi-apps/05-generate-traffic.yaml @@ -0,0 +1,23 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: buybot-job + namespace: default +spec: + template: + metadata: + annotations: + workload: job + labels: + app: buybot + spec: + restartPolicy: Never + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + containers: + - name: curl + image: curlimages/curl:8.4.0 + imagePullPolicy: IfNotPresent + command: [ "curl" ] + args: [ "-s","-X","POST","http://frontend:8080/buy?id=123" ] diff --git a/tests/e2e/multi-apps/assert-apps-installed.yaml b/tests/e2e/multi-apps/assert-apps-installed.yaml new file mode 100644 index 000000000..c78756927 --- /dev/null +++ b/tests/e2e/multi-apps/assert-apps-installed.yaml @@ -0,0 +1,69 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app: frontend + namespace: default +status: + containerStatuses: + - name: frontend + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: coupon + namespace: default +status: + containerStatuses: + - name: coupon + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: inventory + namespace: default +status: + containerStatuses: + - name: inventory + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: membership + namespace: default +status: + containerStatuses: + - name: membership + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: pricing + namespace: default +status: + containerStatuses: + - name: pricing + ready: true + restartCount: 0 + started: true + phase: Running \ No newline at end of file diff --git a/tests/e2e/multi-apps/assert-instrumented-and-pipeline.yaml b/tests/e2e/multi-apps/assert-instrumented-and-pipeline.yaml new file mode 100644 index 000000000..3a014607b --- /dev/null +++ b/tests/e2e/multi-apps/assert-instrumented-and-pipeline.yaml @@ -0,0 +1,319 @@ +apiVersion: odigos.io/v1alpha1 +kind: CollectorsGroup +metadata: + name: odigos-data-collection + namespace: odigos-system +spec: + role: NODE_COLLECTOR +status: + ready: true +--- +apiVersion: odigos.io/v1alpha1 +kind: CollectorsGroup +metadata: + name: odigos-gateway + namespace: odigos-system +spec: + role: CLUSTER_GATEWAY +status: + ready: true +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + odigos.io/collector: "true" + name: odigos-gateway + namespace: odigos-system + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-gateway +spec: + replicas: 1 + selector: + matchLabels: + odigos.io/collector: "true" + template: + metadata: + labels: + odigos.io/collector: "true" + spec: + containers: + - env: + - name: ODIGOS_VERSION + valueFrom: + configMapKeyRef: + key: ODIGOS_VERSION + name: odigos-deployment + - name: GOMEMLIMIT + (value != null): true + name: gateway + resources: + requests: + (memory != null): true + volumeMounts: + - mountPath: /conf + name: collector-conf + volumes: + - configMap: + defaultMode: 420 + items: + - key: collector-conf + path: collector-conf.yaml + name: odigos-gateway + name: collector-conf +status: + availableReplicas: 1 + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: odigos-gateway + namespace: odigos-system + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-gateway +(data != null): true +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: odigos-data-collection + namespace: odigos-system + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-data-collection +(data != null): true +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + odigos.io/data-collection: "true" + name: odigos-data-collection + namespace: odigos-system + ownerReferences: + - apiVersion: odigos.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CollectorsGroup + name: odigos-data-collection +spec: + selector: + matchLabels: + odigos.io/data-collection: "true" + template: + metadata: + labels: + odigos.io/data-collection: "true" + spec: + containers: + - name: data-collection + securityContext: + privileged: true + volumeMounts: + - mountPath: /conf + name: conf + - mountPath: /var/lib/docker/containers + name: varlibdockercontainers + readOnly: true + - mountPath: /var/log + name: varlog + readOnly: true + - mountPath: /var/lib/kubelet/pod-resources + name: kubeletpodresources + readOnly: true + hostNetwork: true + nodeSelector: + kubernetes.io/os: linux + securityContext: {} + serviceAccount: odigos-data-collection + serviceAccountName: odigos-data-collection + volumes: + - configMap: + defaultMode: 420 + items: + - key: conf + path: conf.yaml + name: odigos-data-collection + name: conf + - hostPath: + path: /var/log + type: "" + name: varlog + - hostPath: + path: /var/lib/docker/containers + type: "" + name: varlibdockercontainers + - hostPath: + path: /var/lib/kubelet/pod-resources + type: "" + name: kubeletpodresources +status: + numberAvailable: 1 + numberReady: 1 +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: frontend +spec: + containers: + - name: frontend + resources: + limits: + instrumentation.odigos.io/java-native-community: "1" + requests: + instrumentation.odigos.io/java-native-community: "1" +status: + containerStatuses: + - name: frontend + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: coupon +spec: + containers: + - name: coupon + resources: + limits: + instrumentation.odigos.io/javascript-native-community: "1" + requests: + instrumentation.odigos.io/javascript-native-community: "1" +status: + containerStatuses: + - name: coupon + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: inventory +spec: + containers: + - name: inventory + resources: + limits: + instrumentation.odigos.io/python-native-community: "1" + requests: + instrumentation.odigos.io/python-native-community: "1" +status: + containerStatuses: + - name: inventory + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: membership +spec: + containers: + - name: membership + resources: + limits: + instrumentation.odigos.io/go-ebpf-community: "1" + requests: + instrumentation.odigos.io/go-ebpf-community: "1" +status: + containerStatuses: + - name: membership + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: default + labels: + app: pricing +spec: + containers: + - name: pricing + resources: + limits: + instrumentation.odigos.io/dotnet-native-community: "1" + requests: + instrumentation.odigos.io/dotnet-native-community: "1" +status: + containerStatuses: + - name: pricing + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentationInstance +metadata: + namespace: default + labels: + instrumented-app: deployment-coupon +status: + healthy: true + identifyingAttributes: + - key: service.instance.id + (value != null): true + - key: telemetry.sdk.language + value: nodejs + - key: telemetry.distro.version + value: e2e-test + - key: process.pid + (value != null): true +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentationInstance +metadata: + namespace: default + labels: + instrumented-app: deployment-inventory +status: + healthy: true + identifyingAttributes: + - key: service.instance.id + (value != null): true + - key: process.pid + (value != null): true + - key: telemetry.sdk.language + value: python +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentationInstance +metadata: + namespace: default + labels: + instrumented-app: deployment-membership +status: + healthy: true + reason: LoadedSuccessfully \ No newline at end of file diff --git a/tests/e2e/multi-apps/assert-odigos-installed.yaml b/tests/e2e/multi-apps/assert-odigos-installed.yaml new file mode 100644 index 000000000..5a4671e2b --- /dev/null +++ b/tests/e2e/multi-apps/assert-odigos-installed.yaml @@ -0,0 +1,114 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: odigos-system + labels: + odigos.io/system-object: "true" +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odigos-autoscaler + namespace: odigos-system +status: + containerStatuses: + - name: manager + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odigos-scheduler + namespace: odigos-system +status: + containerStatuses: + - name: manager + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odigos-instrumentor + namespace: odigos-system +status: + containerStatuses: + - name: manager + ready: true + restartCount: 0 + started: true + phase: Running +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: odiglet + namespace: odigos-system + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: DaemonSet + name: odiglet +spec: + containers: + - name: odiglet + resources: {} + securityContext: + capabilities: + add: + - SYS_PTRACE + privileged: true + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + serviceAccount: odiglet + serviceAccountName: odiglet +status: + containerStatuses: + - name: odiglet + ready: true + restartCount: 0 + started: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: destinations.odigos.io +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: instrumentedapplications.odigos.io +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: odigosconfigurations.odigos.io +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + odigos.io/config: "1" + odigos.io/system-object: "true" + name: processors.odigos.io \ No newline at end of file diff --git a/tests/e2e/multi-apps/assert-runtime-detected.yaml b/tests/e2e/multi-apps/assert-runtime-detected.yaml new file mode 100644 index 000000000..f0894f78a --- /dev/null +++ b/tests/e2e/multi-apps/assert-runtime-detected.yaml @@ -0,0 +1,79 @@ +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-coupon + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: coupon +spec: + runtimeDetails: + - containerName: coupon + language: javascript +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-frontend + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: frontend +spec: + runtimeDetails: + - containerName: frontend + language: java +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-inventory + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: inventory +spec: + runtimeDetails: + - containerName: inventory + language: python +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-membership + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: membership +spec: + runtimeDetails: + - containerName: membership + language: go +--- +apiVersion: odigos.io/v1alpha1 +kind: InstrumentedApplication +metadata: + name: deployment-pricing + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: pricing +spec: + runtimeDetails: + - containerName: pricing + language: dotnet diff --git a/tests/e2e/multi-apps/assert-tempo-running.yaml b/tests/e2e/multi-apps/assert-tempo-running.yaml new file mode 100644 index 000000000..f4653f4a3 --- /dev/null +++ b/tests/e2e/multi-apps/assert-tempo-running.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: e2e-tests-tempo-0 + namespace: traces +status: + phase: Running + containerStatuses: + - name: tempo + ready: true + restartCount: 0 \ No newline at end of file diff --git a/tests/e2e/multi-apps/assert-traffic-job-running.yaml b/tests/e2e/multi-apps/assert-traffic-job-running.yaml new file mode 100644 index 000000000..0557b2742 --- /dev/null +++ b/tests/e2e/multi-apps/assert-traffic-job-running.yaml @@ -0,0 +1,10 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: buybot-job + namespace: default +status: + conditions: + - status: "True" + type: Complete + succeeded: 1 diff --git a/tests/e2e/multi-apps/chainsaw-test.yaml b/tests/e2e/multi-apps/chainsaw-test.yaml new file mode 100644 index 000000000..a497a9ec7 --- /dev/null +++ b/tests/e2e/multi-apps/chainsaw-test.yaml @@ -0,0 +1,113 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: multi-apps +spec: + description: This e2e test runs a multi-apps scenario + skipDelete: true + steps: + - name: Prepare destination + try: + - script: + timeout: 60s + content: | + helm repo add grafana https://grafana.github.io/helm-charts + helm repo update + helm install e2e-tests grafana/tempo -n traces --create-namespace --set tempo.storage.trace.block.version=vParquet4 --version 1.10.1 + - assert: + file: assert-tempo-running.yaml + - name: Wait for destination to be ready + try: + - script: + timeout: 60s + content: ../../common/wait_for_dest.sh + - name: Install Odigos + try: + - script: + content: ../../../cli/odigos install --version e2e-test + timeout: 60s + - assert: + file: assert-odigos-installed.yaml + - name: Install Demo App + try: + - script: + timeout: 100s + content: | + docker pull keyval/odigos-demo-inventory:v0.1 + docker pull keyval/odigos-demo-membership:v0.1 + docker pull keyval/odigos-demo-coupon:v0.1 + docker pull keyval/odigos-demo-inventory:v0.1 + docker pull keyval/odigos-demo-frontend:v0.2 + kind load docker-image keyval/odigos-demo-inventory:v0.1 + kind load docker-image keyval/odigos-demo-membership:v0.1 + kind load docker-image keyval/odigos-demo-coupon:v0.1 + kind load docker-image keyval/odigos-demo-inventory:v0.1 + kind load docker-image keyval/odigos-demo-frontend:v0.2 + - apply: + file: 02-install-simple-demo.yaml + - assert: + file: assert-apps-installed.yaml + - name: Detect Languages + try: + - apply: + file: 03-instrument-ns.yaml + - assert: + file: assert-runtime-detected.yaml + - name: Add Destination + try: + - apply: + file: 04-add-destination.yaml + - assert: + file: assert-instrumented-and-pipeline.yaml + - name: Generate Traffic + try: + - script: + timeout: 60s + content: | + while true; do + # Apply the job + kubectl apply -f 05-generate-traffic.yaml + + # Wait for the job to complete + job_name=$(kubectl get -f 05-generate-traffic.yaml -o=jsonpath='{.metadata.name}') + kubectl wait --for=condition=complete job/$job_name + + # Delete the job + kubectl delete -f 05-generate-traffic.yaml + + # Run the wait-for-trace script + ../../common/traceql_runner.sh tracesql/wait-for-trace.yaml + if [ $? -eq 0 ]; then + break + else + sleep 3 + ../../common/flush_traces.sh + fi + done + - name: Verify Trace - Context Propagation + try: + - script: + content: | + ../../common/traceql_runner.sh tracesql/context-propagation.yaml + catch: + - podLogs: + name: odiglet + namespace: odigos-system + - name: Verify Trace - Resource Attributes + try: + - script: + content: | + ../../common/traceql_runner.sh tracesql/resource-attributes.yaml + catch: + - podLogs: + name: odiglet + namespace: odigos-system + - name: Verify Trace - Span Attributes + try: + - script: + content: | + ../../common/traceql_runner.sh tracesql/span-attributes.yaml + catch: + - podLogs: + name: odiglet + namespace: odigos-system diff --git a/tests/e2e/multi-apps/tracesql/context-propagation.yaml b/tests/e2e/multi-apps/tracesql/context-propagation.yaml new file mode 100644 index 000000000..9c463f9b3 --- /dev/null +++ b/tests/e2e/multi-apps/tracesql/context-propagation.yaml @@ -0,0 +1,13 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: This test checks if the context propagation is working correctly between different languages +query: | + { resource.service.name = "frontend" && resource.telemetry.sdk.language = "java" && + span.http.request.method = "POST" && span.http.route = "/buy" && span:kind = server } + >> ( + { resource.service.name = "pricing" && resource.telemetry.sdk.language = "dotnet" } && + { resource.service.name = "inventory" && resource.telemetry.sdk.language = "python" } && + ({ resource.service.name = "coupon" && resource.telemetry.sdk.language = "nodejs" } + >> { resource.service.name = "membership" && resource.telemetry.sdk.language = "go" })) +expected: + count: 1 \ No newline at end of file diff --git a/tests/e2e/multi-apps/tracesql/resource-attributes.yaml b/tests/e2e/multi-apps/tracesql/resource-attributes.yaml new file mode 100644 index 000000000..934439b7e --- /dev/null +++ b/tests/e2e/multi-apps/tracesql/resource-attributes.yaml @@ -0,0 +1,14 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: | + This test check the following resource attributes: + A. odigos.version attribute exists on all spans and has the correct value + B. Kubernetes attributes are correctly set on all spans + At the time of writing this test, TraceQL api does not support not equal to nil so we use regex instead. +query: | + { resource.odigos.version != "e2e-test" || + resource.k8s.deployment.name !~ ".*" || + resource.k8s.node.name !~ "kind-control-plane" || + resource.k8s.pod.name !~ ".*" } +expected: + count: 0 \ No newline at end of file diff --git a/tests/e2e/multi-apps/tracesql/span-attributes.yaml b/tests/e2e/multi-apps/tracesql/span-attributes.yaml new file mode 100644 index 000000000..d508d4a39 --- /dev/null +++ b/tests/e2e/multi-apps/tracesql/span-attributes.yaml @@ -0,0 +1,18 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: | + This test checks the span attributes for a specific trace. + TODO - JS, Python and DotNet SDK are not generating data in latest semconv. add additional checks when they are updated. +query: | + { resource.service.name = "frontend" && resource.telemetry.sdk.language = "java" && + span.http.request.method = "POST" && span.http.route = "/buy" && span:kind = server && + span.http.response.status_code = 200 && span.url.query = "id=123" } + >> ( + { resource.service.name = "pricing" && resource.telemetry.sdk.language = "dotnet" && span:kind = server } && + { resource.service.name = "inventory" && resource.telemetry.sdk.language = "python" && span:kind = server } && + ({ resource.service.name = "coupon" && resource.telemetry.sdk.language = "nodejs" && span:kind = server } + >> { resource.service.name = "membership" && resource.telemetry.sdk.language = "go" && + span.http.request.method = "GET" && span:kind = server && + span.http.response.status_code = 200 && span.url.path = "/isMember" })) +expected: + count: 1 \ No newline at end of file diff --git a/tests/e2e/multi-apps/tracesql/wait-for-trace.yaml b/tests/e2e/multi-apps/tracesql/wait-for-trace.yaml new file mode 100644 index 000000000..a88f58987 --- /dev/null +++ b/tests/e2e/multi-apps/tracesql/wait-for-trace.yaml @@ -0,0 +1,11 @@ +apiVersion: e2e.tests.odigos.io/v1 +kind: TraceTest +description: This test waits for a trace that goes from frontend to pricing, inventory, coupon, and membership services +query: | + { resource.service.name = "frontend" } && + { resource.service.name = "pricing" } && + { resource.service.name = "inventory" } && + { resource.service.name = "coupon" } && + { resource.service.name = "membership" } +expected: + count: 1 \ No newline at end of file