From cd169e47b2514166944f966773d33b6b44800b7d Mon Sep 17 00:00:00 2001 From: MIQUEAS HERRERA Date: Tue, 5 May 2026 12:39:19 -0700 Subject: [PATCH] Add language detection to auto-monitor to inject only the relevant SDK --- go.mod | 9 +- go.sum | 20 + pkg/instrumentation/auto/language_detector.go | 353 +++++++++++++++ .../auto/language_detector_test.go | 423 ++++++++++++++++++ pkg/instrumentation/auto/monitor.go | 33 +- 5 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 pkg/instrumentation/auto/language_detector.go create mode 100644 pkg/instrumentation/auto/language_detector_test.go diff --git a/go.mod b/go.mod index 149fcb6a6..fd019718a 100644 --- a/go.mod +++ b/go.mod @@ -67,11 +67,15 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/digitalocean/godo v1.104.1 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v25.0.13+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect @@ -113,6 +117,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect @@ -162,7 +167,7 @@ require ( github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/ovh/go-ovh v1.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect @@ -176,10 +181,12 @@ require ( github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/vultr/govultr/v2 v2.17.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect diff --git a/go.sum b/go.sum index b5f426dc0..f074de72a 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -116,6 +117,9 @@ github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/T github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -130,8 +134,14 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU= github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -332,6 +342,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg= +github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -562,6 +574,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/openshift/api v0.0.0-20180801171038-322a19404e37 h1:05irGU4HK4IauGGDbsk+ZHrm1wOzMLYjMlfaiqMrBYc= github.com/openshift/api v0.0.0-20180801171038-322a19404e37/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -647,6 +661,7 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -681,6 +696,9 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -911,9 +929,11 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/instrumentation/auto/language_detector.go b/pkg/instrumentation/auto/language_detector.go new file mode 100644 index 000000000..64ac1b06e --- /dev/null +++ b/pkg/instrumentation/auto/language_detector.go @@ -0,0 +1,353 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package auto + +import ( + "context" + "encoding/base64" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecr" + "github.com/go-logr/logr" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + corev1 "k8s.io/api/core/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation" +) + +// languageDetector inspects container image configs from the registry to determine +// the application runtime language. It reads ENV, CMD, and ENTRYPOINT from the +// image manifest without pulling layers. +type languageDetector struct { + logger logr.Logger + keychain authn.Keychain + timeout time.Duration +} + +func newLanguageDetector(logger logr.Logger) *languageDetector { + return &languageDetector{ + logger: logger, + keychain: authn.NewMultiKeychain(newECRKeychain(logger), authn.DefaultKeychain), + timeout: 5 * time.Second, + } +} + +// ecrKeychain uses the AWS SDK default credential chain (instance profile, IRSA, env vars) +// to authenticate with Amazon ECR. This is the same credential chain customers configure +// via aws configure / IAM roles when setting up EKS. +type ecrKeychain struct { + logger logr.Logger +} + +func newECRKeychain(logger logr.Logger) *ecrKeychain { + return &ecrKeychain{logger: logger} +} + +func (k *ecrKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) { + registry := resource.RegistryStr() + if !strings.Contains(registry, ".dkr.ecr.") || !strings.Contains(registry, ".amazonaws.com") { + return authn.Anonymous, nil + } + + sess, err := session.NewSession() + if err != nil { + k.logger.V(2).Info("could not create AWS session for ECR auth", "error", err) + return authn.Anonymous, nil + } + + svc := ecr.New(sess) + result, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{}) + if err != nil { + k.logger.V(2).Info("could not get ECR authorization token", "error", err) + return authn.Anonymous, nil + } + + if len(result.AuthorizationData) == 0 { + return authn.Anonymous, nil + } + + decoded, err := base64.StdEncoding.DecodeString(*result.AuthorizationData[0].AuthorizationToken) + if err != nil { + return authn.Anonymous, nil + } + + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + return authn.Anonymous, nil + } + + return authn.FromConfig(authn.AuthConfig{ + Username: parts[0], + Password: parts[1], + }), nil +} + +// detectLanguages inspects all containers in a pod template and returns the set of +// detected languages. Returns an empty set if no language can be confidently determined. +func (d *languageDetector) detectLanguages(podSpec *corev1.PodTemplateSpec) instrumentation.TypeSet { + detected := make(instrumentation.TypeSet) + + for _, container := range podSpec.Spec.Containers { + if lang := d.detectContainer(container); lang != "" { + d.logger.V(2).Info("detected language from container", + "container", container.Name, "image", container.Image, "language", lang) + detected[lang] = nil + } + } + + return detected +} + +// detectContainer fetches the image config from the registry and inspects it. +// Falls back to pod-spec-only detection if the registry fetch fails. +func (d *languageDetector) detectContainer(container corev1.Container) instrumentation.Type { + // Try fetching real image config from registry + if cfg := d.fetchImageConfig(container.Image); cfg != nil { + if lang := d.detectFromConfig(cfg); lang != "" { + return lang + } + } + + // Fallback: check image name for language patterns (handles private registries where config fetch fails) + if lang := d.detectFromImageName(container.Image); lang != "" { + return lang + } + + // Fallback: check pod-spec-level env vars and commands + if lang := d.detectFromEnvVars(container.Env); lang != "" { + return lang + } + if lang := d.detectFromCommand(container.Command, container.Args); lang != "" { + return lang + } + return "" +} + +// detectFromImageName checks the container image reference string for language indicators. +// This is a heuristic fallback for when registry config fetch is not available. +func (d *languageDetector) detectFromImageName(image string) instrumentation.Type { + lower := strings.ToLower(image) + + javaPatterns := []string{ + "openjdk", "jdk", "jre", "eclipse-temurin", "amazoncorretto", + "corretto", "adoptopenjdk", "ibm-semeru", "graalvm", + "tomcat", "jetty", "wildfly", "quarkus", "springboot", + "spring-boot", "maven", "gradle", "libertycore", "payara", + } + for _, p := range javaPatterns { + if strings.Contains(lower, p) { + return instrumentation.TypeJava + } + } + if strings.Contains(lower, "java") && !strings.Contains(lower, "javascript") { + return instrumentation.TypeJava + } + + pythonPatterns := []string{ + "python", "django", "flask", "fastapi", "uvicorn", + "gunicorn", "celery", "conda", "miniconda", "anaconda", + } + for _, p := range pythonPatterns { + if strings.Contains(lower, p) { + return instrumentation.TypePython + } + } + + nodePatterns := []string{ + "node:", "/node:", "nodejs", "node-", "-node", + "express", "nextjs", "next.js", "nestjs", + } + for _, p := range nodePatterns { + if strings.Contains(lower, p) { + return instrumentation.TypeNodeJS + } + } + + dotnetPatterns := []string{ + "dotnet", "aspnet", "asp.net", "mcr.microsoft.com/dotnet", + } + for _, p := range dotnetPatterns { + if strings.Contains(lower, p) { + return instrumentation.TypeDotNet + } + } + + return "" +} + +// fetchImageConfig retrieves the image config (ENV, CMD, ENTRYPOINT, Labels) from the +// registry. Only fetches the manifest and config blob — no layer data is downloaded. +func (d *languageDetector) fetchImageConfig(imageRef string) *v1.Config { + ref, err := name.ParseReference(imageRef) + if err != nil { + d.logger.V(2).Info("could not parse image reference", "image", imageRef, "error", err) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), d.timeout) + defer cancel() + + desc, err := remote.Get(ref, + remote.WithAuthFromKeychain(d.keychain), + remote.WithContext(ctx), + ) + if err != nil { + d.logger.V(2).Info("could not fetch image descriptor", "image", imageRef, "error", err) + return nil + } + + img, err := desc.Image() + if err != nil { + d.logger.V(2).Info("could not get image from descriptor", "image", imageRef, "error", err) + return nil + } + + cfgFile, err := img.ConfigFile() + if err != nil { + d.logger.V(2).Info("could not read image config", "image", imageRef, "error", err) + return nil + } + + return &cfgFile.Config +} + +// detectFromConfig inspects the image config's ENV, ENTRYPOINT, CMD, and Labels. +func (d *languageDetector) detectFromConfig(cfg *v1.Config) instrumentation.Type { + // Check image-level environment variables + if lang := d.detectFromImageEnv(cfg.Env); lang != "" { + return lang + } + + // Check ENTRYPOINT and CMD + if lang := d.detectFromCommand(cfg.Entrypoint, cfg.Cmd); lang != "" { + return lang + } + + return "" +} + +// detectFromImageEnv checks environment variables from the image config (string slice format: "KEY=VALUE"). +func (d *languageDetector) detectFromImageEnv(envVars []string) instrumentation.Type { + for _, env := range envVars { + parts := strings.SplitN(env, "=", 2) + if len(parts) < 2 { + continue + } + envName := strings.ToUpper(parts[0]) + envValue := strings.ToLower(parts[1]) + + if lang := d.classifyEnv(envName, envValue); lang != "" { + return lang + } + } + return "" +} + +// detectFromEnvVars checks environment variables from the pod spec (corev1.EnvVar format). +func (d *languageDetector) detectFromEnvVars(envVars []corev1.EnvVar) instrumentation.Type { + for _, env := range envVars { + envName := strings.ToUpper(env.Name) + envValue := strings.ToLower(env.Value) + + if lang := d.classifyEnv(envName, envValue); lang != "" { + return lang + } + } + return "" +} + +// classifyEnv determines the language from an env var name and value. +func (d *languageDetector) classifyEnv(name, value string) instrumentation.Type { + switch name { + case "JAVA_HOME", "JAVA_TOOL_OPTIONS", "JAVA_OPTS", + "JVM_OPTS", "CATALINA_HOME", "CATALINA_OPTS", + "MAVEN_HOME", "GRADLE_HOME": + return instrumentation.TypeJava + } + + switch name { + case "PYTHONPATH", "PYTHONHOME", "PYTHONDONTWRITEBYTECODE", + "PYTHONUNBUFFERED", "PIP_NO_CACHE_DIR", + "PYTHON_VERSION", "PYTHON_SHA256", "PYTHON_PIP_VERSION", + "DJANGO_SETTINGS_MODULE", "FLASK_APP": + return instrumentation.TypePython + } + + switch name { + case "NODE_PATH", "NODE_ENV", "NODE_OPTIONS", + "NPM_CONFIG_PREFIX", "YARN_CACHE_FOLDER", + "NODE_VERSION", "YARN_VERSION": + return instrumentation.TypeNodeJS + } + + switch name { + case "DOTNET_ROOT", "ASPNETCORE_URLS", "ASPNETCORE_ENVIRONMENT", + "DOTNET_RUNNING_IN_CONTAINER", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", + "NUGET_PACKAGES", "CORECLR_ENABLE_PROFILING": + return instrumentation.TypeDotNet + } + + if name == "PATH" { + if strings.Contains(value, "/usr/lib/jvm") || strings.Contains(value, "java") { + return instrumentation.TypeJava + } + if strings.Contains(value, "python") { + return instrumentation.TypePython + } + if strings.Contains(value, "/usr/local/lib/node") || strings.Contains(value, "nodejs") { + return instrumentation.TypeNodeJS + } + if strings.Contains(value, "dotnet") { + return instrumentation.TypeDotNet + } + } + return "" +} + +// detectFromCommand checks entrypoint and command args for language indicators. +func (d *languageDetector) detectFromCommand(command []string, args []string) instrumentation.Type { + allParts := append(command, args...) + if len(allParts) == 0 { + return "" + } + + for _, part := range allParts { + lower := strings.ToLower(part) + + if lower == "java" || strings.HasSuffix(lower, "/java") || + strings.HasSuffix(lower, ".jar") || + strings.Contains(lower, "-javaagent:") || + strings.Contains(lower, "org.apache.catalina") || + strings.Contains(lower, "org.springframework") { + return instrumentation.TypeJava + } + + if lower == "python" || lower == "python3" || lower == "python2" || + strings.HasSuffix(lower, "/python") || strings.HasSuffix(lower, "/python3") || + strings.HasSuffix(lower, ".py") || + lower == "gunicorn" || lower == "uvicorn" || lower == "celery" || + lower == "django-admin" || lower == "flask" { + return instrumentation.TypePython + } + + if lower == "node" || lower == "nodejs" || + strings.HasSuffix(lower, "/node") || + strings.HasSuffix(lower, ".js") || strings.HasSuffix(lower, ".mjs") || + lower == "npm" || lower == "yarn" || lower == "npx" || lower == "pnpm" { + return instrumentation.TypeNodeJS + } + + if lower == "dotnet" || strings.HasSuffix(lower, "/dotnet") || + strings.HasSuffix(lower, ".dll") { + return instrumentation.TypeDotNet + } + } + + return "" +} diff --git a/pkg/instrumentation/auto/language_detector_test.go b/pkg/instrumentation/auto/language_detector_test.go new file mode 100644 index 000000000..e2fd6f17f --- /dev/null +++ b/pkg/instrumentation/auto/language_detector_test.go @@ -0,0 +1,423 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package auto + +import ( + "testing" + + "github.com/go-logr/logr" + v1 "github.com/google/go-containerregistry/pkg/v1" + corev1 "k8s.io/api/core/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation" +) + +func newTestDetector() *languageDetector { + return &languageDetector{logger: logr.Discard()} +} + +func TestDetectFromConfig_JavaImages(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + cfg *v1.Config + }{ + { + "openjdk with JAVA_HOME", + &v1.Config{ + Env: []string{"JAVA_HOME=/usr/lib/jvm/java-17-openjdk", "PATH=/usr/lib/jvm/java-17-openjdk/bin:/usr/bin"}, + Cmd: []string{"java", "-jar", "app.jar"}, + }, + }, + { + "corretto with JAVA_TOOL_OPTIONS", + &v1.Config{ + Env: []string{"JAVA_TOOL_OPTIONS=-javaagent:/opt/agent.jar", "JAVA_HOME=/usr/lib/jvm/java-17-amazon-corretto"}, + Entrypoint: []string{"java"}, + Cmd: []string{"-jar", "/app/service.jar"}, + }, + }, + { + "tomcat with CATALINA_HOME", + &v1.Config{ + Env: []string{"CATALINA_HOME=/opt/tomcat", "PATH=/opt/tomcat/bin:/usr/bin"}, + Entrypoint: []string{"catalina.sh"}, + Cmd: []string{"run"}, + }, + }, + { + "spring boot with JVM_OPTS", + &v1.Config{ + Env: []string{"JVM_OPTS=-Xmx512m -Xms256m"}, + Cmd: []string{"java", "-jar", "/app/spring-app.jar"}, + }, + }, + { + "java detected from entrypoint only", + &v1.Config{ + Entrypoint: []string{"java"}, + Cmd: []string{"-jar", "app.jar"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromConfig(tt.cfg) + if result != instrumentation.TypeJava { + t.Errorf("detectFromConfig() = %q, want %q", result, instrumentation.TypeJava) + } + }) + } +} + +func TestDetectFromConfig_PythonImages(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + cfg *v1.Config + }{ + { + "python official image", + &v1.Config{ + Env: []string{"PYTHONPATH=/usr/local/lib/python3.11", "PYTHON_VERSION=3.11.9", "PYTHONDONTWRITEBYTECODE=1"}, + Entrypoint: []string{"python3"}, + }, + }, + { + "django with DJANGO_SETTINGS_MODULE", + &v1.Config{ + Env: []string{"DJANGO_SETTINGS_MODULE=myapp.settings", "PYTHONUNBUFFERED=1"}, + Cmd: []string{"gunicorn", "myapp.wsgi:application"}, + }, + }, + { + "flask app", + &v1.Config{ + Env: []string{"FLASK_APP=app.py"}, + Cmd: []string{"flask", "run", "--host=0.0.0.0"}, + }, + }, + { + "uvicorn from command only", + &v1.Config{ + Cmd: []string{"uvicorn", "main:app", "--host", "0.0.0.0"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromConfig(tt.cfg) + if result != instrumentation.TypePython { + t.Errorf("detectFromConfig() = %q, want %q", result, instrumentation.TypePython) + } + }) + } +} + +func TestDetectFromConfig_NodeImages(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + cfg *v1.Config + }{ + { + "node official image", + &v1.Config{ + Env: []string{"NODE_VERSION=20.11.0", "NODE_ENV=production"}, + Entrypoint: []string{"docker-entrypoint.sh"}, + Cmd: []string{"node"}, + }, + }, + { + "node with NODE_OPTIONS", + &v1.Config{ + Env: []string{"NODE_OPTIONS=--max-old-space-size=4096"}, + Cmd: []string{"node", "server.js"}, + }, + }, + { + "npm start", + &v1.Config{ + Env: []string{"NPM_CONFIG_PREFIX=/home/node/.npm-global"}, + Cmd: []string{"npm", "start"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromConfig(tt.cfg) + if result != instrumentation.TypeNodeJS { + t.Errorf("detectFromConfig() = %q, want %q", result, instrumentation.TypeNodeJS) + } + }) + } +} + +func TestDetectFromConfig_DotNetImages(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + cfg *v1.Config + }{ + { + "aspnet runtime", + &v1.Config{ + Env: []string{"ASPNETCORE_URLS=http://+:8080", "DOTNET_RUNNING_IN_CONTAINER=true"}, + Entrypoint: []string{"dotnet"}, + Cmd: []string{"MyApp.dll"}, + }, + }, + { + "dotnet SDK", + &v1.Config{ + Env: []string{"DOTNET_ROOT=/usr/share/dotnet", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true"}, + Cmd: []string{"dotnet", "run"}, + }, + }, + { + "dotnet from entrypoint only", + &v1.Config{ + Entrypoint: []string{"dotnet", "MyService.dll"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromConfig(tt.cfg) + if result != instrumentation.TypeDotNet { + t.Errorf("detectFromConfig() = %q, want %q", result, instrumentation.TypeDotNet) + } + }) + } +} + +func TestDetectFromConfig_NoDetection(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + cfg *v1.Config + }{ + { + "generic shell entrypoint", + &v1.Config{ + Entrypoint: []string{"/bin/sh", "-c"}, + Cmd: []string{"exec /app/start"}, + }, + }, + { + "nginx", + &v1.Config{ + Env: []string{"NGINX_VERSION=1.25.4", "PATH=/usr/sbin:/usr/bin"}, + Entrypoint: []string{"/docker-entrypoint.sh"}, + Cmd: []string{"nginx", "-g", "daemon off;"}, + }, + }, + { + "empty config", + &v1.Config{}, + }, + { + "only generic env vars", + &v1.Config{ + Env: []string{"APP_PORT=8080", "LOG_LEVEL=info", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromConfig(tt.cfg) + if result != "" { + t.Errorf("detectFromConfig() = %q, want empty (no detection)", result) + } + }) + } +} + +func TestClassifyEnv(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + envName string + envValue string + expected instrumentation.Type + }{ + {"JAVA_HOME", "JAVA_HOME", "/usr/lib/jvm/java-17", instrumentation.TypeJava}, + {"JAVA_OPTS", "JAVA_OPTS", "-Xmx512m", instrumentation.TypeJava}, + {"CATALINA_HOME", "CATALINA_HOME", "/opt/tomcat", instrumentation.TypeJava}, + {"PYTHONPATH", "PYTHONPATH", "/app", instrumentation.TypePython}, + {"DJANGO_SETTINGS", "DJANGO_SETTINGS_MODULE", "myapp.settings", instrumentation.TypePython}, + {"FLASK_APP", "FLASK_APP", "app.py", instrumentation.TypePython}, + {"PYTHONUNBUFFERED", "PYTHONUNBUFFERED", "1", instrumentation.TypePython}, + {"NODE_ENV", "NODE_ENV", "production", instrumentation.TypeNodeJS}, + {"NODE_OPTIONS", "NODE_OPTIONS", "--max-old-space-size=4096", instrumentation.TypeNodeJS}, + {"NODE_VERSION", "NODE_VERSION", "20.11.0", instrumentation.TypeNodeJS}, + {"DOTNET_ROOT", "DOTNET_ROOT", "/usr/share/dotnet", instrumentation.TypeDotNet}, + {"ASPNETCORE_URLS", "ASPNETCORE_URLS", "http://+:8080", instrumentation.TypeDotNet}, + {"ASPNETCORE_ENVIRONMENT", "ASPNETCORE_ENVIRONMENT", "production", instrumentation.TypeDotNet}, + {"PATH with java", "PATH", "/usr/lib/jvm/bin:/usr/bin", instrumentation.TypeJava}, + {"PATH with python", "PATH", "/usr/local/bin/python:/usr/bin", instrumentation.TypePython}, + {"PATH with dotnet", "PATH", "/usr/share/dotnet:/usr/bin", instrumentation.TypeDotNet}, + {"PYTHON_VERSION", "PYTHON_VERSION", "3.11.15", instrumentation.TypePython}, + {"PYTHON_SHA256", "PYTHON_SHA256", "abc123", instrumentation.TypePython}, + {"PYTHON_PIP_VERSION", "PYTHON_PIP_VERSION", "23.0.1", instrumentation.TypePython}, + {"YARN_VERSION", "YARN_VERSION", "1.22.22", instrumentation.TypeNodeJS}, + {"generic env", "APP_PORT", "8080", ""}, + {"empty", "", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.classifyEnv(tt.envName, tt.envValue) + if result != tt.expected { + t.Errorf("classifyEnv(%q, %q) = %q, want %q", tt.envName, tt.envValue, result, tt.expected) + } + }) + } +} + +func TestDetectFromImageName(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + image string + expected instrumentation.Type + }{ + {"openjdk", "public.ecr.aws/docker/library/openjdk:17-slim", instrumentation.TypeJava}, + {"corretto", "amazoncorretto:17", instrumentation.TypeJava}, + {"tomcat", "tomcat:10-jdk17", instrumentation.TypeJava}, + {"java in ecr path", "978751493859.dkr.ecr.us-east-1.amazonaws.com/java-sample-app:latest", instrumentation.TypeJava}, + {"python", "public.ecr.aws/docker/library/python:3.11-slim", instrumentation.TypePython}, + {"django", "mycompany/django-app:latest", instrumentation.TypePython}, + {"node official", "node:20-alpine", instrumentation.TypeNodeJS}, + {"nodejs in name", "mycompany/nodejs-api:v2", instrumentation.TypeNodeJS}, + {"dotnet sdk", "mcr.microsoft.com/dotnet/sdk:8.0", instrumentation.TypeDotNet}, + {"aspnet", "mcr.microsoft.com/dotnet/aspnet:8.0", instrumentation.TypeDotNet}, + {"javascript not java", "mycompany/javascript-tools:latest", ""}, + {"ecr image with python in name", "978751493859.dkr.ecr.us-east-1.amazonaws.com/test-custom-python:latest", instrumentation.TypePython}, + {"truly opaque ecr image", "978751493859.dkr.ecr.us-east-1.amazonaws.com/service-abc:v2.3.1", ""}, + {"alpine", "alpine:3.19", ""}, + {"nginx", "nginx:1.25", ""}, + {"busybox", "busybox:latest", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromImageName(tt.image) + if result != tt.expected { + t.Errorf("detectFromImageName(%q) = %q, want %q", tt.image, result, tt.expected) + } + }) + } +} + +func TestDetectFromCommand(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + command []string + args []string + expected instrumentation.Type + }{ + {"java command", []string{"java"}, []string{"-jar", "app.jar"}, instrumentation.TypeJava}, + {"java full path", []string{"/usr/bin/java"}, []string{"-jar", "app.jar"}, instrumentation.TypeJava}, + {"jar in args", []string{"sh", "-c"}, []string{"java -jar /app/service.jar"}, instrumentation.TypeJava}, + {"python command", []string{"python3"}, []string{"app.py"}, instrumentation.TypePython}, + {"python full path", []string{"/usr/local/bin/python"}, []string{"manage.py"}, instrumentation.TypePython}, + {"gunicorn", []string{"gunicorn"}, []string{"app:app"}, instrumentation.TypePython}, + {"uvicorn", []string{"uvicorn"}, []string{"main:app", "--host", "0.0.0.0"}, instrumentation.TypePython}, + {"flask", []string{"flask"}, []string{"run"}, instrumentation.TypePython}, + {".py file", []string{"python3"}, []string{"/app/main.py"}, instrumentation.TypePython}, + {"node command", []string{"node"}, []string{"server.js"}, instrumentation.TypeNodeJS}, + {"node full path", []string{"/usr/local/bin/node"}, []string{"index.js"}, instrumentation.TypeNodeJS}, + {"npm start", []string{"npm"}, []string{"start"}, instrumentation.TypeNodeJS}, + {"yarn", []string{"yarn"}, []string{"serve"}, instrumentation.TypeNodeJS}, + {".js file", []string{"node"}, []string{"/app/dist/main.js"}, instrumentation.TypeNodeJS}, + {".mjs file", []string{"node"}, []string{"app.mjs"}, instrumentation.TypeNodeJS}, + {"dotnet command", []string{"dotnet"}, []string{"MyApp.dll"}, instrumentation.TypeDotNet}, + {"dotnet full path", []string{"/usr/share/dotnet/dotnet"}, []string{"run"}, instrumentation.TypeDotNet}, + {".dll file", []string{"dotnet"}, []string{"/app/MyService.dll"}, instrumentation.TypeDotNet}, + {"sleep command", []string{"sleep"}, []string{"infinity"}, ""}, + {"shell command", []string{"sh", "-c"}, []string{"echo hello"}, ""}, + {"empty", []string{}, []string{}, ""}, + {"nginx", []string{"nginx"}, []string{"-g", "daemon off;"}, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromCommand(tt.command, tt.args) + if result != tt.expected { + t.Errorf("detectFromCommand(%v, %v) = %q, want %q", tt.command, tt.args, result, tt.expected) + } + }) + } +} + +func TestDetectFromEnvVars_PodSpec(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + env []corev1.EnvVar + expected instrumentation.Type + }{ + {"JAVA_HOME", []corev1.EnvVar{{Name: "JAVA_HOME", Value: "/usr/lib/jvm/java-17"}}, instrumentation.TypeJava}, + {"PYTHONPATH", []corev1.EnvVar{{Name: "PYTHONPATH", Value: "/app"}}, instrumentation.TypePython}, + {"NODE_ENV", []corev1.EnvVar{{Name: "NODE_ENV", Value: "production"}}, instrumentation.TypeNodeJS}, + {"ASPNETCORE_URLS", []corev1.EnvVar{{Name: "ASPNETCORE_URLS", Value: "http://+:8080"}}, instrumentation.TypeDotNet}, + {"generic env", []corev1.EnvVar{{Name: "APP_PORT", Value: "8080"}}, ""}, + {"empty", []corev1.EnvVar{}, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromEnvVars(tt.env) + if result != tt.expected { + t.Errorf("detectFromEnvVars() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestDetectFromImageEnv(t *testing.T) { + d := newTestDetector() + + tests := []struct { + name string + env []string + expected instrumentation.Type + }{ + {"java home", []string{"JAVA_HOME=/usr/lib/jvm/java-17"}, instrumentation.TypeJava}, + {"python version", []string{"PYTHONPATH=/usr/local/lib/python3.11"}, instrumentation.TypePython}, + {"node version", []string{"NODE_VERSION=20.11.0"}, instrumentation.TypeNodeJS}, + {"dotnet root", []string{"DOTNET_ROOT=/usr/share/dotnet"}, instrumentation.TypeDotNet}, + {"multiple envs - java first", []string{"APP_PORT=8080", "JAVA_HOME=/usr/lib/jvm"}, instrumentation.TypeJava}, + {"no signal", []string{"APP_PORT=8080", "LOG_LEVEL=info"}, ""}, + {"PYTHON_VERSION from base image", []string{"PATH=/usr/local/bin:/usr/bin", "PYTHON_VERSION=3.11.15", "PYTHON_SHA256=abc123"}, instrumentation.TypePython}, + {"NODE_VERSION from base image", []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/bin", "NODE_VERSION=20.20.2", "YARN_VERSION=1.22.22"}, instrumentation.TypeNodeJS}, + {"malformed env", []string{"NOEQUALSSIGN"}, ""}, + {"empty", []string{}, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := d.detectFromImageEnv(tt.env) + if result != tt.expected { + t.Errorf("detectFromImageEnv() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/pkg/instrumentation/auto/monitor.go b/pkg/instrumentation/auto/monitor.go index 82eaef7ce..ceb0dc505 100644 --- a/pkg/instrumentation/auto/monitor.go +++ b/pkg/instrumentation/auto/monitor.go @@ -95,6 +95,7 @@ type Monitor struct { deploymentInformer cache.SharedIndexInformer daemonsetInformer cache.SharedIndexInformer statefulsetInformer cache.SharedIndexInformer + langDetector *languageDetector } func (m *Monitor) MutateAndPatchAll(ctx context.Context) { @@ -179,6 +180,7 @@ func NewMonitor(ctx context.Context, config MonitorConfig, k8sClient kubernetes. deploymentInformer: deploymentInformer, daemonsetInformer: daemonsetInformer, statefulsetInformer: statefulSetInformer, + langDetector: newLanguageDetector(logger), } _, err = serviceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -450,7 +452,9 @@ func getTemplateSpecLabels(obj metav1.Object) labels.Set { } } -// MutateObject adds all enabled languages in config. Should only be run if selected by auto monitor or custom selector +// MutateObject adds detected or configured languages. When auto-monitor is active, it first +// attempts to detect the application language from container image, env vars, and commands. +// Falls back to all configured languages only if detection yields no results. func (m *Monitor) MutateObject(oldObj client.Object, obj client.Object) any { if !safeToMutate(oldObj, obj, m.config.RestartPods) { return map[string]string{} @@ -458,8 +462,22 @@ func (m *Monitor) MutateObject(oldObj client.Object, obj client.Object) any { languagesToAnnotate := m.config.CustomSelector.LanguagesOf(obj, false) if m.isWorkloadAutoMonitored(obj) { - for l := range m.config.Languages { - languagesToAnnotate[l] = nil + // Attempt to detect the language from container spec before falling back to all languages + detected := m.detectLanguagesFromWorkload(obj) + if len(detected) > 0 { + m.logger.V(1).Info("auto-monitor detected language(s) from container spec", + "objName", obj.GetName(), "detected", detected) + for l := range detected { + if _, ok := m.config.Languages[l]; ok { + languagesToAnnotate[l] = nil + } + } + } else { + m.logger.V(1).Info("auto-monitor could not detect language, falling back to all configured languages", + "objName", obj.GetName(), "languages", m.config.Languages) + for l := range m.config.Languages { + languagesToAnnotate[l] = nil + } } } @@ -471,6 +489,15 @@ func (m *Monitor) MutateObject(oldObj client.Object, obj client.Object) any { return mutate(obj, languagesToAnnotate) } +// detectLanguagesFromWorkload extracts the pod template from a workload and runs language detection. +func (m *Monitor) detectLanguagesFromWorkload(obj client.Object) instrumentation.TypeSet { + podTemplate := getPodTemplate(obj) + if podTemplate == nil { + return nil + } + return m.langDetector.detectLanguages(podTemplate) +} + // returns if workload is auto monitored (does not include custom selector) func (m *Monitor) isWorkloadAutoMonitored(obj client.Object) bool { if isNamespace(obj) {