From 0df64e908f4d8c12deda50f817ee9856cf3e9558 Mon Sep 17 00:00:00 2001 From: Rauno Viskus <1334536+rauno56@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:14:10 +0300 Subject: [PATCH 01/22] fix: match file names in helm registry index and release assets (#1373) Adding a separate Helm chart for CRDs. :exclamation: The charts are not strictly dependent to one another and need to be separately managed and installed. Meaning we need to document that both of those need to be updated and installed. Fair questions that might arise: **Why isn't CRD chart a declared dependency for `odigos`?** Helm merges dependent resources together with the ones from dependencies. We'd be exactly where we are right now if we did that. **Why not use the official `crd` support?** Helm really doesn't "support" CRDs. It's an opt-out feature of "we don't know how we want to do it so we don't for now". Resources declared as CRDs(different from declaring them under `templates`) are not updated. **How do other charts do it then?** All charts I've encountered(cert-manager, jaeger, contour) have CRDs under a flag and include them in the same chart as the rest of the resources **but then** do not depend on them in in the install. CRs are created either only by the user or later in the applications life-cycle. This doesn't work for us since we declare Odigos Config among resources in the application chart. That begs the question whether Odigos Config must be a CR at all or could it be a config map instead - we wouldn't get "type-checking" by k8s API but would gain some flexibility and could release our charts as one. --- .github/workflows/release.yml | 7 +++++-- helm/odigos/Chart.yaml | 8 ++++---- scripts/release-charts.sh | 30 ++++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 14 deletions(-) mode change 100644 => 100755 scripts/release-charts.sh 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/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/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" From 793503c365998503af0b67a44d02b671488e8606 Mon Sep 17 00:00:00 2001 From: tamirdavid1 Date: Mon, 22 Jul 2024 16:12:06 +0300 Subject: [PATCH 02/22] Avoid app to crash python agent (#1385) Co-authored-by: Tamir David --- agents/python/configurator/__init__.py | 16 +++++++--- agents/python/configurator/lib_handling.py | 36 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 agents/python/configurator/lib_handling.py 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) From 81388a5c0e5fb47651aa4931d97837816f925b70 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Wed, 24 Jul 2024 15:19:01 +0300 Subject: [PATCH 03/22] refactor: use instance uid as connection key in opamp server (#1390) --- odiglet/Dockerfile | 2 +- opampserver/pkg/connection/conncache.go | 20 ++++++++++---------- opampserver/pkg/server/server.go | 15 +++++++++++---- 3 files changed, 22 insertions(+), 15 deletions(-) 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/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/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 From 90a5a09c04db9aac4a3d1a395cefc0f85384767b Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Wed, 24 Jul 2024 16:24:12 +0300 Subject: [PATCH 04/22] feat: store container name for each instrumentation instance (#1392) --- .../odigos.io_instrumentationinstances.yaml | 8 +++ .../v1alpha1/instrumentationinstance.go | 7 ++- .../v1alpha1/instrumentationinstancespec.go | 38 +++++++++++++ .../odigos/applyconfiguration/utils.go | 2 + .../v1alpha1/instrumentationinstance_types.go | 14 ++++- cli/cmd/describe.go | 3 ++ .../pkg/instrumentation_instance/status.go | 8 ++- odiglet/pkg/ebpf/director.go | 54 ++++++++++--------- opampserver/pkg/connection/types.go | 1 + opampserver/pkg/server/handlers.go | 3 +- 10 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 api/generated/odigos/applyconfiguration/odigos/v1alpha1/instrumentationinstancespec.go 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/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/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/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), From c2db8648b5f20b0819135556bfed6628e60203b3 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Thu, 25 Jul 2024 11:29:10 +0300 Subject: [PATCH 05/22] chore: init setup sources components --- .../webapp/app/setup/choose-sources/page.tsx | 17 +- frontend/webapp/app/setup/layout.tsx | 7 +- .../webapp/components/setup/menu/index.tsx | 4 +- frontend/webapp/public/icons/common/info.svg | 3 + .../webapp/public/icons/common/search.svg | 3 + frontend/webapp/reuseable-components/index.ts | 2 + .../reuseable-components/input/index.tsx | 184 ++++++++++++++++++ .../section-title/index.tsx | 57 ++++++ .../reuseable-components/text/index.tsx | 8 +- .../reuseable-components/tooltip/index.tsx | 69 +++++++ frontend/webapp/styles/theme.ts | 4 +- 11 files changed, 345 insertions(+), 13 deletions(-) create mode 100644 frontend/webapp/public/icons/common/info.svg create mode 100644 frontend/webapp/public/icons/common/search.svg create mode 100644 frontend/webapp/reuseable-components/input/index.tsx create mode 100644 frontend/webapp/reuseable-components/section-title/index.tsx create mode 100644 frontend/webapp/reuseable-components/tooltip/index.tsx diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index 933dfb802..cca8a2243 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useSuspenseQuery, gql } from '@apollo/client'; +import { Input, SectionTitle } from '@/reuseable-components'; const GET_COMPUTE_PLATFORM = gql` query GetComputePlatform($cpId: ID!) { @@ -41,5 +42,19 @@ export default function ChooseSourcesPage() { variables: { cpId: '1' }, }); - return <>; + return ( +
+ + +
+ ); } diff --git a/frontend/webapp/app/setup/layout.tsx b/frontend/webapp/app/setup/layout.tsx index f3afba447..d03aa3117 100644 --- a/frontend/webapp/app/setup/layout.tsx +++ b/frontend/webapp/app/setup/layout.tsx @@ -26,7 +26,6 @@ const MainContent = styled.div` display: flex; max-width: 1440px; width: 100%; - background-color: ${({ theme }) => theme.colors.secondary}; flex-direction: column; align-items: center; `; @@ -44,11 +43,7 @@ export default function SetupLayout({ - - - - {children} - + {children} ); } diff --git a/frontend/webapp/components/setup/menu/index.tsx b/frontend/webapp/components/setup/menu/index.tsx index aa217815e..08e1716f5 100644 --- a/frontend/webapp/components/setup/menu/index.tsx +++ b/frontend/webapp/components/setup/menu/index.tsx @@ -108,9 +108,9 @@ const SideMenu: React.FC = () => { )} - {step.title} + {step.title} {step.subtitle && ( - + {step.subtitle} )} 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/index.ts b/frontend/webapp/reuseable-components/index.ts index c912adb8d..b95b1445a 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -1,2 +1,4 @@ export * from './text'; export * from './button'; +export * from './section-title'; +export * from './input'; diff --git a/frontend/webapp/reuseable-components/input/index.tsx b/frontend/webapp/reuseable-components/input/index.tsx new file mode 100644 index 000000000..a1b04c5bc --- /dev/null +++ b/frontend/webapp/reuseable-components/input/index.tsx @@ -0,0 +1,184 @@ +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; + padding: 0 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; +`; + +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; +`; + +const Input: React.FC = ({ + icon, + buttonLabel, + onButtonClick, + errorMessage, + title, + tooltip, + ...props +}) => { + return ( + + + + {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..71e8afc9d 100644 --- a/frontend/webapp/reuseable-components/text/index.tsx +++ b/frontend/webapp/reuseable-components/text/index.tsx @@ -8,6 +8,7 @@ interface TextProps { weight?: number; align?: 'left' | 'center' | 'right'; family?: 'primary' | 'secondary'; + opacity?: number; } const TextWrapper = styled.span<{ @@ -16,12 +17,13 @@ const TextWrapper = styled.span<{ 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; @@ -40,6 +42,7 @@ const Text: React.FC = ({ weight = 400, align = 'left', family = 'primary', + opacity = 1, }) => { return ( = ({ size={size} weight={weight} align={align} + opacity={opacity} > {children} diff --git a/frontend/webapp/reuseable-components/tooltip/index.tsx b/frontend/webapp/reuseable-components/tooltip/index.tsx new file mode 100644 index 000000000..0a88f7358 --- /dev/null +++ b/frontend/webapp/reuseable-components/tooltip/index.tsx @@ -0,0 +1,69 @@ +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` + &:hover ${TooltipText} { + visibility: visible; + opacity: 1; + z-index: 999; + } +`; + +const Tooltip: React.FC = ({ text, children }) => { + return ( + + + {children} + + {text} + + + + ); +}; + +export { Tooltip }; diff --git a/frontend/webapp/styles/theme.ts b/frontend/webapp/styles/theme.ts index 6789e96cf..5da9b382d 100644 --- a/frontend/webapp/styles/theme.ts +++ b/frontend/webapp/styles/theme.ts @@ -18,8 +18,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 From e4448247944205f63b1cf363f84462e7e62519cd Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Thu, 25 Jul 2024 14:06:55 +0300 Subject: [PATCH 06/22] chore: init tooltip component --- .../webapp/app/setup/choose-sources/page.tsx | 3 --- .../reuseable-components/input/index.tsx | 24 ++++++++++++------- .../reuseable-components/tooltip/index.tsx | 24 ++++++++++++------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index cca8a2243..57ee736ad 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -51,9 +51,6 @@ export default function ChooseSourcesPage() { ); diff --git a/frontend/webapp/reuseable-components/input/index.tsx b/frontend/webapp/reuseable-components/input/index.tsx index a1b04c5bc..b52900018 100644 --- a/frontend/webapp/reuseable-components/input/index.tsx +++ b/frontend/webapp/reuseable-components/input/index.tsx @@ -133,6 +133,7 @@ const HeaderWrapper = styled.div` display: flex; align-items: center; gap: 6px; + margin-bottom: 4px; `; const Input: React.FC = ({ @@ -146,14 +147,21 @@ const Input: React.FC = ({ }) => { return ( - - - {title} - {tooltip && ( - - )} - - + {title && ( + + + {title} + {tooltip && ( + + )} + + + )} ` &:hover ${TooltipText} { - visibility: visible; - opacity: 1; - z-index: 999; + ${({ hasText }) => + hasText && + ` + visibility: visible; + opacity: 1; + z-index: 999; + `} } `; const Tooltip: React.FC = ({ text, children }) => { + const hasText = !!text; + return ( - + {children} - - {text} - + {hasText && ( + + {text} + + )} ); From 4632da30f91d03ccfd34df017cc2a4c0fe3d2c42 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Thu, 25 Jul 2024 14:59:30 +0300 Subject: [PATCH 07/22] chore: added dropdoen component --- .../webapp/app/setup/choose-sources/page.tsx | 32 ++- .../components/setup/headers/header/index.tsx | 2 +- .../public/icons/common/extend-arrow.svg | 5 + .../reuseable-components/divider/index.tsx | 36 ++++ .../reuseable-components/dropdown/index.tsx | 187 ++++++++++++++++++ frontend/webapp/reuseable-components/index.ts | 3 + .../reuseable-components/input/index.tsx | 3 +- frontend/webapp/styles/theme.ts | 1 + 8 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 frontend/webapp/public/icons/common/extend-arrow.svg create mode 100644 frontend/webapp/reuseable-components/divider/index.tsx create mode 100644 frontend/webapp/reuseable-components/dropdown/index.tsx diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index 57ee736ad..ca6425492 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -1,8 +1,8 @@ 'use client'; -import React from 'react'; +import React, { useState } from 'react'; import { useSuspenseQuery, gql } from '@apollo/client'; -import { Input, SectionTitle } from '@/reuseable-components'; +import { Dropdown, Input, SectionTitle } from '@/reuseable-components'; const GET_COMPUTE_PLATFORM = gql` query GetComputePlatform($cpId: ID!) { @@ -42,16 +42,34 @@ export default function ChooseSourcesPage() { variables: { cpId: '1' }, }); + const [selectedOption, setSelectedOption] = useState('All types'); + const options = [ + 'All types', + 'Existing destinations', + 'Self hosted', + 'Managed', + ]; + return ( -
+
- +
+ + + +
); } 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 + + + + \ No newline at end of file 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..a61e47386 --- /dev/null +++ b/frontend/webapp/reuseable-components/dropdown/index.tsx @@ -0,0 +1,187 @@ +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'; + +interface DropdownProps { + options: string[]; + selectedOption: string; + onSelect: (option: string) => 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: rgba(249, 249, 249, 0.08); + border: 1px solid ${({ theme }) => theme.colors.border}; + border-radius: 32px; + margin-top: 4px; + z-index: 10; +`; + +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); + + const filteredOptions = options.filter((option) => + option.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleSelect = (option: string) => { + onSelect(option); + setIsOpen(false); + }; + + return ( + + {title && ( + + + {title} + {tooltip && ( + + )} + + + )} + setIsOpen(!isOpen)}> + {selectedOption} + + + + {isOpen && ( + + + setSearchTerm(e.target.value)} + /> + + + {filteredOptions.map((option) => ( + handleSelect(option)} + > + {option} + + {option === selectedOption && ( + + )} + + ))} + + )} + + ); +}; + +export { Dropdown }; diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index b95b1445a..2740c05e6 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -2,3 +2,6 @@ export * from './text'; export * from './button'; export * from './section-title'; export * from './input'; +export * from './tooltip'; +export * from './dropdown'; +export * from './divider'; diff --git a/frontend/webapp/reuseable-components/input/index.tsx b/frontend/webapp/reuseable-components/input/index.tsx index b52900018..9b62c8bc1 100644 --- a/frontend/webapp/reuseable-components/input/index.tsx +++ b/frontend/webapp/reuseable-components/input/index.tsx @@ -30,7 +30,7 @@ const InputWrapper = styled.div<{ align-items: center; height: 36px; gap: 12px; - padding: 0 12px; + transition: border-color 0.3s; border-radius: 32px; border: 1px solid rgba(249, 249, 249, 0.24); @@ -89,6 +89,7 @@ const StyledInput = styled.input` const IconWrapper = styled.div` display: flex; align-items: center; + margin-left: 12px; `; const Button = styled.button` diff --git a/frontend/webapp/styles/theme.ts b/frontend/webapp/styles/theme.ts index 5da9b382d..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 = { From f12cde690ec028de655c368787624f6fde7135a5 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Thu, 25 Jul 2024 15:38:05 +0300 Subject: [PATCH 08/22] chore: counter component --- .../webapp/app/setup/choose-sources/page.tsx | 16 ++++++-- .../reuseable-components/counter/index.tsx | 39 +++++++++++++++++++ frontend/webapp/reuseable-components/index.ts | 1 + .../reuseable-components/text/index.tsx | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 frontend/webapp/reuseable-components/counter/index.tsx diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index ca6425492..8f791b6a2 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -2,7 +2,13 @@ import React, { useState } from 'react'; import { useSuspenseQuery, gql } from '@apollo/client'; -import { Dropdown, Input, SectionTitle } from '@/reuseable-components'; +import { + Counter, + Divider, + Dropdown, + Input, + SectionTitle, +} from '@/reuseable-components'; const GET_COMPUTE_PLATFORM = gql` query GetComputePlatform($cpId: ID!) { @@ -56,7 +62,7 @@ export default function ChooseSourcesPage() { title="Choose sources" description="Apps will be automatically instrumented, and data will be sent to the relevant APM's destinations." /> -
+
+ +
+ +
); } 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/index.ts b/frontend/webapp/reuseable-components/index.ts index 2740c05e6..48b6b8186 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -5,3 +5,4 @@ export * from './input'; export * from './tooltip'; export * from './dropdown'; export * from './divider'; +export * from './counter'; diff --git a/frontend/webapp/reuseable-components/text/index.tsx b/frontend/webapp/reuseable-components/text/index.tsx index 71e8afc9d..7044c019e 100644 --- a/frontend/webapp/reuseable-components/text/index.tsx +++ b/frontend/webapp/reuseable-components/text/index.tsx @@ -39,7 +39,7 @@ const Text: React.FC = ({ children, color, size = 16, - weight = 400, + weight = 300, align = 'left', family = 'primary', opacity = 1, From 154fc7d312bda4a1da447cb5fba50a57756853b7 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Thu, 25 Jul 2024 16:19:05 +0300 Subject: [PATCH 09/22] chore: wip --- .../webapp/app/setup/choose-sources/page.tsx | 27 +++++- .../reuseable-components/checkbox/index.tsx | 82 +++++++++++++++++++ frontend/webapp/reuseable-components/index.ts | 2 + .../reuseable-components/toggle/index.tsx | 80 ++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 frontend/webapp/reuseable-components/checkbox/index.tsx create mode 100644 frontend/webapp/reuseable-components/toggle/index.tsx diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index 8f791b6a2..902358931 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -3,11 +3,13 @@ import React, { useState } from 'react'; import { useSuspenseQuery, gql } from '@apollo/client'; import { + Checkbox, Counter, Divider, Dropdown, Input, SectionTitle, + Toggle, } from '@/reuseable-components'; const GET_COMPUTE_PLATFORM = gql` @@ -56,8 +58,12 @@ export default function ChooseSourcesPage() { 'Managed', ]; + const handleCheckboxChange = (value: boolean) => { + console.log('Checkbox is now', value); + }; + return ( -
+
-
+
+
+ + +
+
+
); } 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/index.ts b/frontend/webapp/reuseable-components/index.ts index 48b6b8186..4090df8fb 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -6,3 +6,5 @@ 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/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 }; From 9272ba9117fbe8b8c836d1c400b752f62608dfa6 Mon Sep 17 00:00:00 2001 From: Tamir David Date: Thu, 25 Jul 2024 16:49:41 +0300 Subject: [PATCH 10/22] doc: improve docs and add python document (#1393) Co-authored-by: Tamir David --- docs/instrumentations/golang/golang.mdx | 2 +- docs/instrumentations/nodejs/enrichment.mdx | 12 +++ docs/instrumentations/python/enrichment.mdx | 96 +++++++++++++++++++ docs/instrumentations/python/python.mdx | 88 +++++++++++++++++ docs/mint.json | 9 +- .../actions/sampling/introduction.mdx | 6 +- 6 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 docs/instrumentations/python/enrichment.mdx create mode 100644 docs/instrumentations/python/python.mdx 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/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. From ecc564dd457f7c52dc9f43d3d913a36b42e8b58d Mon Sep 17 00:00:00 2001 From: Tamir David Date: Thu, 25 Jul 2024 17:00:52 +0300 Subject: [PATCH 11/22] capitalize-pii-title (#1395) Co-authored-by: Tamir David --- docs/pipeline/actions/attributes/piimasking.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From a29e4bbfcfe62c674fb347c977d0c62495491dd6 Mon Sep 17 00:00:00 2001 From: Eden Federman Date: Thu, 25 Jul 2024 19:02:02 +0300 Subject: [PATCH 12/22] Improve Odigos UI Memory consumption (#1394) * Use Pagination for large queries from Kubernetes API Server * When possible query only for metadata and not full objects (for example in the count objects in namespace logic) --- frontend/endpoints/applications.go | 102 ++++++++++++++++------------- frontend/endpoints/namespaces.go | 42 ++++++------ frontend/kube/client.go | 18 +++-- k8sutils/pkg/client/pager.go | 30 +++++++++ 4 files changed, 119 insertions(+), 73 deletions(-) create mode 100644 k8sutils/pkg/client/pager.go diff --git a/frontend/endpoints/applications.go b/frontend/endpoints/applications.go index fd9f294a6..a29346cf0 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) { @@ -126,70 +130,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..b6f660e28 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" @@ -177,31 +181,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/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/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 +} From 645076e0e682bccaa8df5694113b73f393966540 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:57:36 +0300 Subject: [PATCH 13/22] chore(deps): bump the otel-dependencies group across 1 directory with 3 updates (#1364) --- odiglet/go.mod | 56 ++++++++++++----------- odiglet/go.sum | 122 ++++++++++++++++++++++++++----------------------- 2 files changed, 94 insertions(+), 84 deletions(-) diff --git a/odiglet/go.mod b/odiglet/go.mod index d6fa2beff..8702bf531 100644 --- a/odiglet/go.mod +++ b/odiglet/go.mod @@ -14,9 +14,9 @@ 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 @@ -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..8a5e2ac92 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= From 72d36624bd38dbf7288c9a22bdede421a19a4212 Mon Sep 17 00:00:00 2001 From: Eden Federman Date: Sun, 28 Jul 2024 09:04:30 +0300 Subject: [PATCH 14/22] Improve E2E tests (#1265) --- .github/workflows/e2e.yaml | 91 ++--- .github/workflows/e2e/bats/utilities.bash | 166 --------- .github/workflows/e2e/bats/verify.bats | 165 --------- .../workflows/e2e/collector-helm-values.yaml | 49 --- .github/workflows/e2e/jaeger-dest.yaml | 12 - .gitignore | 4 - Makefile | 18 +- tests/common/flush_traces.sh | 20 ++ tests/common/traceql_runner.sh | 74 ++++ tests/common/wait_for_dest.sh | 24 ++ tests/e2e/README.md | 101 ++++++ .../helm-chart/02-install-simple-demo.yaml | 124 ++++--- tests/e2e/helm-chart/03-instrument-ns.yaml | 6 + tests/e2e/helm-chart/04-add-destination.yaml | 12 + .../e2e/helm-chart/05-generate-traffic.yaml | 5 +- .../e2e/helm-chart/assert-apps-installed.yaml | 69 ++++ .../assert-instrumented-and-pipeline.yaml | 319 ++++++++++++++++++ .../helm-chart/assert-odigos-installed.yaml | 114 +++++++ .../helm-chart/assert-runtime-detected.yaml | 79 +++++ .../e2e/helm-chart/assert-tempo-running.yaml | 11 + .../assert-traffic-job-running.yaml | 10 + tests/e2e/helm-chart/chainsaw-test.yaml | 130 +++++++ .../tracesql/context-propagation.yaml | 13 + .../tracesql/resource-attributes.yaml | 14 + .../helm-chart/tracesql/span-attributes.yaml | 18 + .../helm-chart/tracesql/wait-for-trace.yaml | 11 + .../multi-apps/02-install-simple-demo.yaml | 203 +++++++++++ tests/e2e/multi-apps/03-instrument-ns.yaml | 6 + tests/e2e/multi-apps/04-add-destination.yaml | 12 + tests/e2e/multi-apps/05-generate-traffic.yaml | 23 ++ .../e2e/multi-apps/assert-apps-installed.yaml | 69 ++++ .../assert-instrumented-and-pipeline.yaml | 319 ++++++++++++++++++ .../multi-apps/assert-odigos-installed.yaml | 114 +++++++ .../multi-apps/assert-runtime-detected.yaml | 79 +++++ .../e2e/multi-apps/assert-tempo-running.yaml | 11 + .../assert-traffic-job-running.yaml | 10 + tests/e2e/multi-apps/chainsaw-test.yaml | 113 +++++++ .../tracesql/context-propagation.yaml | 13 + .../tracesql/resource-attributes.yaml | 14 + .../multi-apps/tracesql/span-attributes.yaml | 18 + .../multi-apps/tracesql/wait-for-trace.yaml | 11 + 41 files changed, 2146 insertions(+), 528 deletions(-) delete mode 100644 .github/workflows/e2e/bats/utilities.bash delete mode 100644 .github/workflows/e2e/bats/verify.bats delete mode 100644 .github/workflows/e2e/collector-helm-values.yaml delete mode 100644 .github/workflows/e2e/jaeger-dest.yaml create mode 100755 tests/common/flush_traces.sh create mode 100755 tests/common/traceql_runner.sh create mode 100755 tests/common/wait_for_dest.sh create mode 100644 tests/e2e/README.md rename .github/workflows/e2e/kv-shop.yaml => tests/e2e/helm-chart/02-install-simple-demo.yaml (70%) create mode 100644 tests/e2e/helm-chart/03-instrument-ns.yaml create mode 100644 tests/e2e/helm-chart/04-add-destination.yaml rename .github/workflows/e2e/buybot-job.yaml => tests/e2e/helm-chart/05-generate-traffic.yaml (77%) create mode 100644 tests/e2e/helm-chart/assert-apps-installed.yaml create mode 100644 tests/e2e/helm-chart/assert-instrumented-and-pipeline.yaml create mode 100644 tests/e2e/helm-chart/assert-odigos-installed.yaml create mode 100644 tests/e2e/helm-chart/assert-runtime-detected.yaml create mode 100644 tests/e2e/helm-chart/assert-tempo-running.yaml create mode 100644 tests/e2e/helm-chart/assert-traffic-job-running.yaml create mode 100644 tests/e2e/helm-chart/chainsaw-test.yaml create mode 100644 tests/e2e/helm-chart/tracesql/context-propagation.yaml create mode 100644 tests/e2e/helm-chart/tracesql/resource-attributes.yaml create mode 100644 tests/e2e/helm-chart/tracesql/span-attributes.yaml create mode 100644 tests/e2e/helm-chart/tracesql/wait-for-trace.yaml create mode 100644 tests/e2e/multi-apps/02-install-simple-demo.yaml create mode 100644 tests/e2e/multi-apps/03-instrument-ns.yaml create mode 100644 tests/e2e/multi-apps/04-add-destination.yaml create mode 100644 tests/e2e/multi-apps/05-generate-traffic.yaml create mode 100644 tests/e2e/multi-apps/assert-apps-installed.yaml create mode 100644 tests/e2e/multi-apps/assert-instrumented-and-pipeline.yaml create mode 100644 tests/e2e/multi-apps/assert-odigos-installed.yaml create mode 100644 tests/e2e/multi-apps/assert-runtime-detected.yaml create mode 100644 tests/e2e/multi-apps/assert-tempo-running.yaml create mode 100644 tests/e2e/multi-apps/assert-traffic-job-running.yaml create mode 100644 tests/e2e/multi-apps/chainsaw-test.yaml create mode 100644 tests/e2e/multi-apps/tracesql/context-propagation.yaml create mode 100644 tests/e2e/multi-apps/tracesql/resource-attributes.yaml create mode 100644 tests/e2e/multi-apps/tracesql/span-attributes.yaml create mode 100644 tests/e2e/multi-apps/tracesql/wait-for-trace.yaml 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/.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/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 From 91a79591659817af7aff4b67ebc2ff73b4ea6fb7 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 28 Jul 2024 09:43:54 +0300 Subject: [PATCH 15/22] chore: wip --- frontend/graph/schema.graphqls | 1 - frontend/graph/schema.resolvers.go | 6 +++++- frontend/webapp/app/setup/choose-sources/page.tsx | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) 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..753bf7fc3 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -39,8 +39,12 @@ func (r *queryResolver) ComputePlatform(ctx context.Context, cpID string) (*mode res[i] = k8sThinSourceToGql(&source) } + name := "odigos-system" + return &model.ComputePlatform{ - K8sActualSources: res, + K8sActualSources: res, + Name: &name, + ComputePlatformType: model.ComputePlatformTypeK8s, }, nil } diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index 902358931..37c0862bb 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useSuspenseQuery, gql } from '@apollo/client'; import { @@ -50,6 +50,10 @@ export default function ChooseSourcesPage() { variables: { cpId: '1' }, }); + useEffect(() => { + console.log(data); + }, [data]); + const [selectedOption, setSelectedOption] = useState('All types'); const options = [ 'All types', From dd944a5665a4c94ed73de66d7ac479710b4e378a Mon Sep 17 00:00:00 2001 From: Eden Federman Date: Sun, 28 Jul 2024 10:07:39 +0300 Subject: [PATCH 16/22] Add additional ClickHouse fields (#1396) --- common/config/clickhouse.go | 57 ++++++++++++++++++++++++++----- destinations/data/clickhouse.yaml | 39 ++++++++++++++++++++- 2 files changed, 87 insertions(+), 9 deletions(-) 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 From 718ed4014a4a27a56a6d3759766ee0e67ebee87a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:26:10 -0700 Subject: [PATCH 17/22] chore(deps): bump the k8s-dependencies group across 1 directory with 3 updates (#1384) Bumps the k8s-dependencies group with 3 updates in the /odiglet directory: [k8s.io/api](https://github.com/kubernetes/api), [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/kubelet](https://github.com/kubernetes/kubelet). Updates `k8s.io/api` from 0.30.2 to 0.30.3
Commits

Updates `k8s.io/client-go` from 0.30.2 to 0.30.3
Commits

Updates `k8s.io/kubelet` from 0.30.2 to 0.30.3
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- odiglet/go.mod | 6 +++--- odiglet/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/odiglet/go.mod b/odiglet/go.mod index 8702bf531..7ab7a28c2 100644 --- a/odiglet/go.mod +++ b/odiglet/go.mod @@ -19,10 +19,10 @@ require ( 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 ) diff --git a/odiglet/go.sum b/odiglet/go.sum index 8a5e2ac92..6b52b66a2 100644 --- a/odiglet/go.sum +++ b/odiglet/go.sum @@ -585,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= @@ -605,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= From 7ef7a223e84a29b9f64e1719550d99151c3d9196 Mon Sep 17 00:00:00 2001 From: Alon Braymok <138359965+alonkeyval@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:30:57 +0300 Subject: [PATCH 18/22] [GEN-1160] fix: create action bug (#1397) create action button was disabled all the time --- .../actions.forms/add.cluster.info/index.tsx | 15 +++++ .../actions.forms/delete.attribute/index.tsx | 23 +++++-- .../dynamic.action.form/index.tsx | 67 ++++++++----------- .../actions.forms/rename.attributes/index.tsx | 21 +++++- .../samplers/error-sampler/index.tsx | 19 +++++- .../samplers/latency-action/index.tsx | 13 +--- .../samplers/probabilistic-sampler/index.tsx | 17 ++++- .../main/actions/edit-action/index.tsx | 2 +- .../main/actions/edit-action/styled.ts | 8 ++- 9 files changed, 123 insertions(+), 62 deletions(-) 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/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` From 412d448336f757f43cd4a2b1b6fd3f44b529addb Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 28 Jul 2024 15:03:45 +0300 Subject: [PATCH 19/22] chore: get compute platform namspace with actual sources --- frontend/endpoints/actual-sources.go | 21 ++++------ frontend/endpoints/applications.go | 22 ++++++++++ frontend/endpoints/namespaces.go | 40 +++++++++++++++++++ frontend/graph/conversions.go | 11 +++++ frontend/graph/schema.resolvers.go | 17 ++++++++ .../reuseable-components/dropdown/index.tsx | 4 +- 6 files changed, 100 insertions(+), 15 deletions(-) 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 a29346cf0..a16df40e6 100644 --- a/frontend/endpoints/applications.go +++ b/frontend/endpoints/applications.go @@ -75,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. diff --git a/frontend/endpoints/namespaces.go b/frontend/endpoints/namespaces.go index b6f660e28..f0f6ff23d 100644 --- a/frontend/endpoints/namespaces.go +++ b/frontend/endpoints/namespaces.go @@ -73,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) { 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.resolvers.go b/frontend/graph/schema.resolvers.go index 753bf7fc3..d54ccbe98 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -40,11 +40,28 @@ func (r *queryResolver) ComputePlatform(ctx context.Context, cpID string) (*mode } 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, Name: &name, ComputePlatformType: model.ComputePlatformTypeK8s, + K8sActualNamespaces: K8sActualNamespaces, }, nil } diff --git a/frontend/webapp/reuseable-components/dropdown/index.tsx b/frontend/webapp/reuseable-components/dropdown/index.tsx index a61e47386..f6a2c2a6e 100644 --- a/frontend/webapp/reuseable-components/dropdown/index.tsx +++ b/frontend/webapp/reuseable-components/dropdown/index.tsx @@ -60,11 +60,11 @@ const DropdownListContainer = styled.div` flex-direction: column; gap: 8px; padding: 8px; - background-color: rgba(249, 249, 249, 0.08); + background-color: #242424; border: 1px solid ${({ theme }) => theme.colors.border}; border-radius: 32px; margin-top: 4px; - z-index: 10; + z-index: 999; `; const SearchInputContainer = styled.div` From 5242d162c0776f2a63de8551819fe560f6319076 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 28 Jul 2024 16:06:43 +0300 Subject: [PATCH 20/22] chore: sources list --- .../webapp/app/setup/choose-sources/page.tsx | 60 +++++---- .../containers/setup/sources/sources-list.tsx | 123 ++++++++++++++++++ .../webapp/public/icons/common/folder.svg | 3 + .../reuseable-components/text/index.tsx | 2 +- 4 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 frontend/webapp/containers/setup/sources/sources-list.tsx create mode 100644 frontend/webapp/public/icons/common/folder.svg diff --git a/frontend/webapp/app/setup/choose-sources/page.tsx b/frontend/webapp/app/setup/choose-sources/page.tsx index 37c0862bb..2812c568d 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -11,6 +11,7 @@ import { SectionTitle, Toggle, } from '@/reuseable-components'; +import { SourcesList } from '@/containers/setup/sources/sources-list'; const GET_COMPUTE_PLATFORM = gql` query GetComputePlatform($cpId: ID!) { @@ -18,37 +19,43 @@ const GET_COMPUTE_PLATFORM = gql` id name computePlatformType - k8sActualSources { - namespace - kind + k8sActualNamespaces { name - serviceName - autoInstrumented - creationTimestamp - numberOfInstances - hasInstrumentedApplication - instrumentedApplicationDetails { - languages { - containerName - language - } - conditions { - type - status - lastTransitionTime - reason - message - } + k8sActualSources { + name + kind + numberOfInstances } } } } `; +type ComputePlatformData = { + id: string; + name: string; + computePlatformType: string; + k8sActualNamespaces: { + name: string; + k8sActualSources: { + name: string; + kind: string; + numberOfInstances: number; + }[]; + }[]; +}; + +type ComputePlatform = { + computePlatform: ComputePlatformData; +}; + export default function ChooseSourcesPage() { - const { error, data } = useSuspenseQuery(GET_COMPUTE_PLATFORM, { - variables: { cpId: '1' }, - }); + const { error, data } = useSuspenseQuery( + GET_COMPUTE_PLATFORM, + { + variables: { cpId: '1' }, + } + ); useEffect(() => { console.log(data); @@ -104,7 +111,12 @@ export default function ChooseSourcesPage() { onChange={handleCheckboxChange} />
- + +
); } diff --git a/frontend/webapp/containers/setup/sources/sources-list.tsx b/frontend/webapp/containers/setup/sources/sources-list.tsx new file mode 100644 index 000000000..4e823e45d --- /dev/null +++ b/frontend/webapp/containers/setup/sources/sources-list.tsx @@ -0,0 +1,123 @@ +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; +} + +const SourcesList: React.FC<{ items: K8sActualSource[] }> = ({ items }) => { + const [selectedItems, setSelectedItems] = useState([]); + + const handleItemClick = (name: string) => { + setSelectedItems((prevSelectedItems) => + prevSelectedItems.includes(name) + ? prevSelectedItems.filter((item) => item !== name) + : [...prevSelectedItems, name] + ); + }; + + return ( + + {items.map((item) => ( + handleItemClick(item.name)} + > + + + source + + + {item.name} + + {item.numberOfInstances} running instances · {item.kind} + + + + {selectedItems.includes(item.name) && ( + + + SELECTED + + + )} + + ))} + + ); +}; + +export { SourcesList }; 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/reuseable-components/text/index.tsx b/frontend/webapp/reuseable-components/text/index.tsx index 7044c019e..a375ed19a 100644 --- a/frontend/webapp/reuseable-components/text/index.tsx +++ b/frontend/webapp/reuseable-components/text/index.tsx @@ -11,7 +11,7 @@ interface TextProps { opacity?: number; } -const TextWrapper = styled.span<{ +const TextWrapper = styled.div<{ color?: string; size: number; weight: number; From fc9e1fccd0bdc8bccd1daec28f9c9149dc3f632d Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 29 Jul 2024 14:09:24 +0300 Subject: [PATCH 21/22] chore: wip --- .../app/(setup)/choose-sources/page.tsx | 47 +------ .../webapp/app/setup/choose-sources/page.tsx | 121 +--------------- frontend/webapp/app/setup/layout.tsx | 9 +- .../choose-sources-list/index.tsx | 131 ++++++++++++++++++ .../choose-sources-menu/index.ts | 2 + .../search-and-dropdown.tsx | 37 +++++ .../toggles-and-checkboxes.tsx | 60 ++++++++ .../choose-sources-menu/type.ts | 30 ++++ .../main/sources/choose-sources/index.tsx | 124 +++++++++++++++++ .../webapp/containers/main/sources/index.ts | 1 + .../graphql/queries/compute-platform.ts | 19 +++ frontend/webapp/graphql/queries/index.ts | 1 + .../webapp/hooks/compute-platform/index.ts | 1 + .../compute-platform/useComputePlatform.ts | 20 +++ frontend/webapp/hooks/index.tsx | 1 + .../reuseable-components/dropdown/index.tsx | 24 ++-- frontend/webapp/types/common.ts | 5 + frontend/webapp/types/compute-platform.ts | 16 +++ frontend/webapp/types/index.ts | 1 + frontend/webapp/types/sources.ts | 6 + 20 files changed, 483 insertions(+), 173 deletions(-) create mode 100644 frontend/webapp/containers/main/sources/choose-sources/choose-sources-list/index.tsx create mode 100644 frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/index.ts create mode 100644 frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/search-and-dropdown.tsx create mode 100644 frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/toggles-and-checkboxes.tsx create mode 100644 frontend/webapp/containers/main/sources/choose-sources/choose-sources-menu/type.ts create mode 100644 frontend/webapp/containers/main/sources/choose-sources/index.tsx create mode 100644 frontend/webapp/graphql/queries/compute-platform.ts create mode 100644 frontend/webapp/hooks/compute-platform/index.ts create mode 100644 frontend/webapp/hooks/compute-platform/useComputePlatform.ts create mode 100644 frontend/webapp/types/compute-platform.ts 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 2812c568d..3c0fbed5a 100644 --- a/frontend/webapp/app/setup/choose-sources/page.tsx +++ b/frontend/webapp/app/setup/choose-sources/page.tsx @@ -1,122 +1,11 @@ 'use client'; -import React, { useEffect, useState } from 'react'; - -import { useSuspenseQuery, gql } from '@apollo/client'; -import { - Checkbox, - Counter, - Divider, - Dropdown, - Input, - SectionTitle, - Toggle, -} from '@/reuseable-components'; -import { SourcesList } from '@/containers/setup/sources/sources-list'; - -const GET_COMPUTE_PLATFORM = gql` - query GetComputePlatform($cpId: ID!) { - computePlatform(cpId: $cpId) { - id - name - computePlatformType - k8sActualNamespaces { - name - k8sActualSources { - name - kind - numberOfInstances - } - } - } - } -`; - -type ComputePlatformData = { - id: string; - name: string; - computePlatformType: string; - k8sActualNamespaces: { - name: string; - k8sActualSources: { - name: string; - kind: string; - numberOfInstances: number; - }[]; - }[]; -}; - -type ComputePlatform = { - computePlatform: ComputePlatformData; -}; +import React from 'react'; +import { ChooseSourcesContainer } from '@/containers/main'; export default function ChooseSourcesPage() { - const { error, data } = useSuspenseQuery( - GET_COMPUTE_PLATFORM, - { - variables: { cpId: '1' }, - } - ); - - useEffect(() => { - console.log(data); - }, [data]); - - const [selectedOption, setSelectedOption] = useState('All types'); - const options = [ - 'All types', - 'Existing destinations', - 'Self hosted', - 'Managed', - ]; - - const handleCheckboxChange = (value: boolean) => { - console.log('Checkbox is now', value); - }; - return ( -
- -
- - - -
- -
- -
- - -
- -
- - -
+ <> + + ); } diff --git a/frontend/webapp/app/setup/layout.tsx b/frontend/webapp/app/setup/layout.tsx index d03aa3117..198d6164d 100644 --- a/frontend/webapp/app/setup/layout.tsx +++ b/frontend/webapp/app/setup/layout.tsx @@ -30,6 +30,11 @@ const MainContent = styled.div` align-items: center; `; +const ContentWrapper = styled.div` + width: 640px; + padding-top: 64px; +`; + export default function SetupLayout({ children, }: { @@ -43,7 +48,9 @@ export default function SetupLayout({ - {children} + + {children} + ); } 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/reuseable-components/dropdown/index.tsx b/frontend/webapp/reuseable-components/dropdown/index.tsx index f6a2c2a6e..b43634117 100644 --- a/frontend/webapp/reuseable-components/dropdown/index.tsx +++ b/frontend/webapp/reuseable-components/dropdown/index.tsx @@ -5,11 +5,13 @@ 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: string[]; - selectedOption: string; - onSelect: (option: string) => void; + options: DropdownOption[]; + selectedOption: DropdownOption | undefined; + onSelect: (option: DropdownOption) => void; title?: string; tooltip?: string; } @@ -112,11 +114,13 @@ const Dropdown: React.FC = ({ const [searchTerm, setSearchTerm] = useState(''); const dropdownRef = useRef(null); + useOnClickOutside(dropdownRef, () => setIsOpen(false)); + const filteredOptions = options.filter((option) => - option.toLowerCase().includes(searchTerm.toLowerCase()) + option.value.toLowerCase().includes(searchTerm.toLowerCase()) ); - const handleSelect = (option: string) => { + const handleSelect = (option: DropdownOption) => { onSelect(option); setIsOpen(false); }; @@ -139,7 +143,7 @@ const Dropdown: React.FC = ({ )} setIsOpen(!isOpen)}> - {selectedOption} + {selectedOption?.value} = ({ {filteredOptions.map((option) => ( handleSelect(option)} > - {option} + {option.value} - {option === selectedOption && ( + {option.id === selectedOption?.id && ( Date: Mon, 29 Jul 2024 14:17:10 +0300 Subject: [PATCH 22/22] chore: wip --- .../containers/setup/sources/sources-list.tsx | 123 ------------------ 1 file changed, 123 deletions(-) delete mode 100644 frontend/webapp/containers/setup/sources/sources-list.tsx diff --git a/frontend/webapp/containers/setup/sources/sources-list.tsx b/frontend/webapp/containers/setup/sources/sources-list.tsx deleted file mode 100644 index 4e823e45d..000000000 --- a/frontend/webapp/containers/setup/sources/sources-list.tsx +++ /dev/null @@ -1,123 +0,0 @@ -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; -} - -const SourcesList: React.FC<{ items: K8sActualSource[] }> = ({ items }) => { - const [selectedItems, setSelectedItems] = useState([]); - - const handleItemClick = (name: string) => { - setSelectedItems((prevSelectedItems) => - prevSelectedItems.includes(name) - ? prevSelectedItems.filter((item) => item !== name) - : [...prevSelectedItems, name] - ); - }; - - return ( - - {items.map((item) => ( - handleItemClick(item.name)} - > - - - source - - - {item.name} - - {item.numberOfInstances} running instances · {item.kind} - - - - {selectedItems.includes(item.name) && ( - - - SELECTED - - - )} - - ))} - - ); -}; - -export { SourcesList };