Skip to content

Commit

Permalink
feat: Collect Language Version in Runtime (#1434)
Browse files Browse the repository at this point in the history
  • Loading branch information
yodigos authored Aug 15, 2024
1 parent 50866ca commit 9fe3d90
Show file tree
Hide file tree
Showing 17 changed files with 226 additions and 84 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,8 @@ cli-install:
@echo "Installing odigos from source. version: $(ODIGOS_CLI_VERSION)"
go run -tags=embed_manifests ./cli install --version $(ODIGOS_CLI_VERSION)


.PHONY: cli-upgrade
cli-upgrade:
@echo "Installing odigos from source. version: $(ODIGOS_CLI_VERSION)"
go run -tags=embed_manifests ./cli upgrade --version $(ODIGOS_CLI_VERSION) --yes
2 changes: 2 additions & 0 deletions api/config/crd/bases/odigos.io_instrumentedapplications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ spec:
- unknown
- ignored
type: string
runtimeVersion:
type: string
required:
- containerName
- language
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 13 additions & 12 deletions api/odigos/v1alpha1/instrumentedapplication_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,42 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

//+kubebuilder:object:generate=true
// +kubebuilder:object:generate=true
type ConfigOption struct {
OptionKey string `json:"optionKey"`
OptionKey string `json:"optionKey"`
SpanKind common.SpanKind `json:"spanKind"`
}

//+kubebuilder:object:generate=true
// +kubebuilder:object:generate=true
type InstrumentationLibraryOptions struct {
LibraryName string `json:"libraryName"`
Options []ConfigOption `json:"options"`
LibraryName string `json:"libraryName"`
Options []ConfigOption `json:"options"`
}

//+kubebuilder:object:generate=true
// +kubebuilder:object:generate=true
type EnvVar struct {
Name string `json:"name"`
Value string `json:"value"`
}

//+kubebuilder:object:generate=true
// +kubebuilder:object:generate=true
type RuntimeDetailsByContainer struct {
ContainerName string `json:"containerName"`
Language common.ProgrammingLanguage `json:"language"`
EnvVars []EnvVar `json:"envVars,omitempty"`
ContainerName string `json:"containerName"`
Language common.ProgrammingLanguage `json:"language"`
RuntimeVersion string `json:"runtimeVersion,omitempty"`
EnvVars []EnvVar `json:"envVars,omitempty"`
}

// +kubebuilder:object:generate=true
type OptionByContainer struct {
ContainerName string `json:"containerName"`
ContainerName string `json:"containerName"`
InstrumentationLibraries []InstrumentationLibraryOptions `json:"instrumentationsLibraries"`
}

// InstrumentedApplicationSpec defines the desired state of InstrumentedApplication
type InstrumentedApplicationSpec struct {
RuntimeDetails []RuntimeDetailsByContainer `json:"runtimeDetails,omitempty"`
Options []OptionByContainer `json:"options,omitempty"`
Options []OptionByContainer `json:"options,omitempty"`
}

// InstrumentedApplicationStatus defines the observed state of InstrumentedApplication
Expand Down
5 changes: 5 additions & 0 deletions common/lang_detection.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package common

type ProgramLanguageDetails struct {
Language ProgrammingLanguage
RuntimeVersion string
}

// +kubebuilder:validation:Enum=java;python;go;dotnet;javascript;mysql;unknown;ignored
type ProgrammingLanguage string

Expand Down
2 changes: 2 additions & 0 deletions helm/odigos/templates/crds/instrumentedapplications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ spec:
- unknown
- ignored
type: string
runtimeVersion:
type: string
required:
- containerName
- language
Expand Down
57 changes: 50 additions & 7 deletions odiglet/debug.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
FROM python:3.11 AS python-builder
ARG ODIGOS_VERSION
WORKDIR /python-instrumentation
ADD odiglet/agents/python/requirements.txt .
RUN mkdir workspace && pip install --target workspace -r requirements.txt
COPY ../agents/python ./agents/python
RUN echo "VERSION = \"$ODIGOS_VERSION\";" > ./agents/python/configurator/version.py
RUN mkdir workspace && pip install ./agents/python/ --target workspace

FROM node:16 AS nodejs-builder

######### Node.js Native Community Agent #########
#
# The Node.js agent is built in multiple stages so it can be built with either upstream
# @odigos/opentelemetry-node or with a local clone to test changes during development.
# The implemntation is based on the following blog post:
# https://www.docker.com/blog/dockerfiles-now-support-multiple-build-contexts/

# The first build stage 'nodejs-agent-native-community-clone' clones the agent sources from github main branch.
FROM alpine AS nodejs-agent-native-community-clone
RUN apk add git
WORKDIR /src
ARG NODEJS_AGENT_VERSION=main
RUN git clone https://github.com/odigos-io/opentelemetry-node.git && cd opentelemetry-node && git checkout $NODEJS_AGENT_VERSION

# The second build stage 'nodejs-agent-native-community-src' prepares the actual code we are going to compile and embed in odiglet.
# By default, it uses the previous 'nodejs-agent-native-community-src' stage, but one can override it by setting the
# --build-context nodejs-agent-native-community-src=../opentelemetry-node flag in the docker build command.
# This allows us to nobe the agent sources and test changes during development.
# The output of this stage is the resolved source code to be used in the next stage.
FROM scratch AS nodejs-agent-native-community-src
COPY --from=nodejs-agent-native-community-clone /src/opentelemetry-node /

# The third build stage 'nodejs-agent-native-community-builder' compiles the agent sources and prepares the final output.
# it COPY from the previous 'nodejs-agent-native-community-src' stage, so it can be used with either the upstream or local sources.
# The output of this stage is the compiled agent code in:
# - package source code in '/nodejs-instrumentation/build/src' directory.
# - all required dependencies in '/nodejs-instrumentation/prod_node_modules' directory.
# These artifacts are later copied into the odiglet final image to be mounted into auto-instrumented pods at runtime.
FROM node:18 AS nodejs-agent-native-community-builder
ARG ODIGOS_VERSION
WORKDIR /nodejs-instrumentation
COPY odiglet/agents/nodejs .
RUN npm install
COPY --from=nodejs-agent-native-community-src /package.json /yarn.lock ./
# prepare the production node_modules content in a separate directory
RUN yarn --production --frozen-lockfile
RUN mv node_modules ./prod_node_modules
# install all dependencies including dev so we can yarn compile
RUN yarn --frozen-lockfile
COPY --from=nodejs-agent-native-community-src / ./
# inject the actual version into the agent code
RUN echo "export const VERSION = \"$ODIGOS_VERSION\";" > ./src/version.ts
RUN yarn compile

FROM busybox:1.36.1 AS dotnet-builder
WORKDIR /dotnet-instrumentation
Expand All @@ -23,13 +63,14 @@ RUN ARCH_SUFFIX=$(cat /tmp/arch_suffix) && \
unzip opentelemetry-dotnet-instrumentation-linux-glibc-${ARCH_SUFFIX}.zip && \
rm opentelemetry-dotnet-instrumentation-linux-glibc-${ARCH_SUFFIX}.zip

FROM --platform=$BUILDPLATFORM keyval/odiglet-base:v1.5 as builder
FROM --platform=$BUILDPLATFORM keyval/odiglet-base:v1.5 AS builder
WORKDIR /go/src/github.com/odigos-io/odigos
# Copyy local modules required by the build
COPY api/ api/
COPY common/ common/
COPY k8sutils/ k8sutils/
COPY procdiscovery/ procdiscovery/
COPY opampserver/ opampserver/
WORKDIR /go/src/github.com/odigos-io/odigos/odiglet
COPY odiglet/ .

Expand All @@ -52,7 +93,9 @@ RUN chmod 644 /instrumentations/java/javaagent.jar
COPY --from=python-builder /python-instrumentation/workspace /instrumentations/python

# NodeJS
COPY --from=nodejs-builder /nodejs-instrumentation/build/workspace /instrumentations/nodejs
COPY --from=nodejs-agent-native-community-builder /nodejs-instrumentation/build/src /instrumentations/nodejs
COPY --from=nodejs-agent-native-community-builder /nodejs-instrumentation/prod_node_modules /instrumentations/nodejs/node_modules


# .NET
COPY --from=dotnet-builder /dotnet-instrumentation /instrumentations/dotnet
Expand Down
20 changes: 11 additions & 9 deletions odiglet/pkg/kube/runtime_details/inspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ func runtimeInspection(pods []corev1.Pod, ignoredContainers []string) ([]odigosv
continue
}

var lang common.ProgrammingLanguage
programLanguageDetails := common.ProgramLanguageDetails{Language: common.UnknownProgrammingLanguage}
var inspectProc *procdiscovery.Details
var detectErr error

for _, proc := range processes {
lang, detectErr = inspectors.DetectLanguage(proc)
if detectErr == nil && lang != common.UnknownProgrammingLanguage {
programLanguageDetails, detectErr = inspectors.DetectLanguage(proc)
if detectErr == nil && programLanguageDetails.Language != common.UnknownProgrammingLanguage {
inspectProc = &proc
break
}
Expand All @@ -108,22 +108,24 @@ func runtimeInspection(pods []corev1.Pod, ignoredContainers []string) ([]odigosv
envs := make([]odigosv1.EnvVar, 0)
if inspectProc == nil {
log.Logger.V(0).Info("unable to detect language for any process", "pod", pod.Name, "container", container.Name, "namespace", pod.Namespace)
lang = common.UnknownProgrammingLanguage
programLanguageDetails.Language = common.UnknownProgrammingLanguage
} else {
if len(processes) > 1 {
log.Logger.V(0).Info("multiple processes found in pod container, only taking the first one with detected language into account", "pod", pod.Name, "container", container.Name, "namespace", pod.Namespace)
}

// Convert map to slice for k8s format
envs = make([]odigosv1.EnvVar, 0, len(inspectProc.Envs))
for envName, envValue := range inspectProc.Envs {
envs = make([]odigosv1.EnvVar, 0, len(inspectProc.Environments.DetailedEnvs))
for envName, envValue := range inspectProc.Environments.OverwriteEnvs {
envs = append(envs, odigosv1.EnvVar{Name: envName, Value: envValue})
}
}

resultsMap[container.Name] = odigosv1.RuntimeDetailsByContainer{
ContainerName: container.Name,
Language: lang,
EnvVars: envs,
ContainerName: container.Name,
Language: programLanguageDetails.Language,
RuntimeVersion: programLanguageDetails.RuntimeVersion,
EnvVars: envs,
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions procdiscovery/pkg/inspectors/dotnet/dotnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ const (
dotnet = "DOTNET"
)

func (d *DotnetInspector) Inspect(p *process.Details) (common.ProgrammingLanguage, bool) {
func (d *DotnetInspector) Inspect(p *process.Details) (common.ProgramLanguageDetails, bool) {
var programLanguageDetails common.ProgramLanguageDetails
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/environ", p.ProcessID))
if err == nil {
environ := string(data)
if strings.Contains(environ, aspnet) || strings.Contains(environ, dotnet) {
return common.DotNetProgrammingLanguage, true
programLanguageDetails.Language = common.DotNetProgrammingLanguage
return programLanguageDetails, true
}
}

return "", false
return programLanguageDetails, false
}
14 changes: 10 additions & 4 deletions procdiscovery/pkg/inspectors/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import (

type GolangInspector struct{}

func (g *GolangInspector) Inspect(p *process.Details) (common.ProgrammingLanguage, bool) {
func (g *GolangInspector) Inspect(p *process.Details) (common.ProgramLanguageDetails, bool) {
var programLanguageDetails common.ProgramLanguageDetails
file := fmt.Sprintf("/proc/%d/exe", p.ProcessID)
_, err := buildinfo.ReadFile(file)
buildInfo, err := buildinfo.ReadFile(file)
if err != nil {
return "", false
return programLanguageDetails, false
}

return common.GoProgrammingLanguage, true
programLanguageDetails.Language = common.GoProgrammingLanguage
if buildInfo != nil {
programLanguageDetails.RuntimeVersion = buildInfo.GoVersion
}

return programLanguageDetails, true
}
15 changes: 11 additions & 4 deletions procdiscovery/pkg/inspectors/java/java.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ type JavaInspector struct{}

const processName = "java"

func (j *JavaInspector) Inspect(p *process.Details) (common.ProgrammingLanguage, bool) {
if strings.Contains(p.ExeName, processName) || strings.Contains(p.CmdLine, processName) {
return common.JavaProgrammingLanguage, true
func (j *JavaInspector) Inspect(proc *process.Details) (common.ProgramLanguageDetails, bool) {
var programLanguageDetails common.ProgramLanguageDetails

if strings.Contains(proc.ExeName, processName) || strings.Contains(proc.CmdLine, processName) {
programLanguageDetails.Language = common.JavaProgrammingLanguage
if value, exists := proc.GetDetailedEnvsValue(process.JavaVersionConst); exists {
programLanguageDetails.RuntimeVersion = value
}

return programLanguageDetails, true
}

return "", false
return programLanguageDetails, false
}
31 changes: 18 additions & 13 deletions procdiscovery/pkg/inspectors/langdetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (e ErrLanguageDetectionConflict) Error() string {
}

type inspector interface {
Inspect(process *process.Details) (common.ProgrammingLanguage, bool)
Inspect(process *process.Details) (common.ProgramLanguageDetails, bool)
}

var inspectorsList = []inspector{
Expand All @@ -37,23 +37,28 @@ var inspectorsList = []inspector{
// DetectLanguage returns the detected language for the process or
// common.UnknownProgrammingLanguage if the language could not be detected, in which case error == nil
// if error or language detectors disagree common.UnknownProgrammingLanguage is also returned
func DetectLanguage(process process.Details) (common.ProgrammingLanguage, error) {
detectedLanguage := common.UnknownProgrammingLanguage
func DetectLanguage(process process.Details) (common.ProgramLanguageDetails, error) {
detectedProgramLanguageDetails := common.ProgramLanguageDetails{
Language: common.UnknownProgrammingLanguage,
}

for _, i := range inspectorsList {
language, detected := i.Inspect(&process)
languageDetails, detected := i.Inspect(&process)
if detected {
if detectedLanguage == common.UnknownProgrammingLanguage {
detectedLanguage = language
if detectedProgramLanguageDetails.Language == common.UnknownProgrammingLanguage {
detectedProgramLanguageDetails = languageDetails
continue
}
return common.UnknownProgrammingLanguage, ErrLanguageDetectionConflict{
languages: [2]common.ProgrammingLanguage{
detectedLanguage,
language,
},
}
return common.ProgramLanguageDetails{
Language: common.UnknownProgrammingLanguage,
}, ErrLanguageDetectionConflict{
languages: [2]common.ProgrammingLanguage{
detectedProgramLanguageDetails.Language,
languageDetails.Language,
},
}
}
}

return detectedLanguage, nil
return detectedProgramLanguageDetails, nil
}
8 changes: 5 additions & 3 deletions procdiscovery/pkg/inspectors/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ type MySQLInspector struct{}

const MySQLProcessName = "mysqld"

func (j *MySQLInspector) Inspect(p *process.Details) (common.ProgrammingLanguage, bool) {
func (j *MySQLInspector) Inspect(p *process.Details) (common.ProgramLanguageDetails, bool) {
var programLanguageDetails common.ProgramLanguageDetails
if strings.HasSuffix(p.ExeName, MySQLProcessName) || strings.HasSuffix(p.CmdLine, MySQLProcessName) {
return common.MySQLProgrammingLanguage, true
programLanguageDetails.Language = common.MySQLProgrammingLanguage
return programLanguageDetails, true
}

return "", false
return programLanguageDetails, false
}
Loading

0 comments on commit 9fe3d90

Please sign in to comment.