Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to auto-instrument with the OTel Java SDK #1704

Merged
merged 23 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1201,3 +1201,6 @@ test/integration/components/testserver_1.17/vendor/gopkg.in/yaml.v2/writerc.go
test/integration/components/testserver_1.17/vendor/gopkg.in/yaml.v2/yaml.go
test/integration/components/testserver_1.17/vendor/gopkg.in/yaml.v2/yamlh.go
test/integration/components/testserver_1.17/vendor/gopkg.in/yaml.v2/yamlprivateh.go
*_bpfel.go
*_bpfel.o
pkg/internal/otelsdk/grafana-opentelemetry-java.jar
17 changes: 12 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,19 @@ update-offsets: prereqs

# Prefer docker-generate instead, as it's able to perform a parallel build
.PHONY: generate
generate: export BPF_CLANG := $(CLANG)
generate: export BPF_CFLAGS := $(CFLAGS)
generate: export BPF2GO := $(BPF2GO)
generate: bpf2go
generate: bpf-generate javaagent-generate

.PHONY: bpf-generate
bpf-generate: export BPF_CLANG := $(CLANG)
bpf-generate: export BPF_CFLAGS := $(CFLAGS)
bpf-generate: export BPF2GO := $(BPF2GO)
bpf-generate: bpf2go
@echo "### Generating BPF Go bindings"
go generate ./pkg/...
grep -rlI "BPF2GO" pkg/internal/ | xargs -P 0 -n 1 go generate

.PHONY: javaagent-generate
javaagent-generate:
@go generate pkg/internal/otelsdk/sdk_inject.go

.PHONY: docker-generate
docker-generate:
Expand Down
4 changes: 3 additions & 1 deletion generator.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM golang:alpine3.21 AS base
ARG EBPF_VER

# Installs dependencies that are required to compile eBPF programs
RUN apk add clang llvm19
RUN apk add clang llvm19 curl
RUN apk cache purge
RUN go install github.com/cilium/ebpf/cmd/bpf2go@$EBPF_VER

Expand All @@ -21,6 +21,8 @@ export BPF_CFLAGS="-O2 -g -Wall -Werror"

export GENFILES=\$1

go generate pkg/internal/otelsdk/sdk_inject.go

if [ -z "\$GENFILES" ]; then
echo No genfiles specified - regenerating everything
grep -rlI "BPF2GO" pkg/internal/ | xargs -P 0 -n 1 go generate
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/grafana/beyla/v2

go 1.23.0
go 1.23.2

require (
github.com/AlessandroPomponio/go-gibberish v0.0.0-20191004143433-a2d4156f0396
Expand All @@ -17,6 +17,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/grafana/go-offsets-tracker v0.1.7
github.com/grafana/jvmtools v0.0.3
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/go-offsets-tracker v0.1.7 h1:2zBQ7iiGzvyXY7LA8kaaSiEqH/Yx82UcfRabbY5aOG4=
github.com/grafana/go-offsets-tracker v0.1.7/go.mod h1:qcQdu7zlUKIFNUdBJlLyNHuJGW0SKWKjkrN6jtt+jds=
github.com/grafana/jvmtools v0.0.3 h1:4uPquep5v+54Z04OQYOCldrv2SR42IRagHQXrNHsc4g=
github.com/grafana/jvmtools v0.0.3/go.mod h1:b0tiPI5zNyIuPUz2DwvxUCI+5bFoG7A4i47F9sc8w98=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/ebpf_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ type EBPFTracer struct {

// Enables debug printing of the protocol data
ProtocolDebug bool `yaml:"protocol_debug_print" env:"BEYLA_PROTOCOL_DEBUG_PRINT"`

// Enables Java instrumentation with the OpenTelemetry JDK Agent
UseOTelSDKForJava bool `yaml:"use_otel_sdk_for_java" env:"BEYLA_USE_OTEL_SDK_FOR_JAVA"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the code above, I assume that enabling this would only generate OTEL metrics or traces. I think we should validate that if Prometheus is enabled this option can't be enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this happens right now:
https://github.com/grafana/beyla/pull/1704/files#diff-d6000537195fb5cd7aa9e0b6bea705c332ce53d5db9e070e39dffcdc191562e3R65

With prometheus scrape Metrics will be false, so we'll program it to send traces only.

}
38 changes: 27 additions & 11 deletions pkg/export/otel/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
envProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL"
envHeaders = "OTEL_EXPORTER_OTLP_HEADERS"
envTracesHeaders = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"
envMetricsHeaders = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"
envResourceAttrs = "OTEL_RESOURCE_ATTRIBUTES"
)

Expand Down Expand Up @@ -229,8 +230,7 @@ type otlpOptions struct {
BaseURLPath string
URLPath string
SkipTLSVerify bool
HTTPHeaders map[string]string
GRPCHeaders map[string]string
Headers map[string]string
}

func (o *otlpOptions) AsMetricHTTP() []otlpmetrichttp.Option {
Expand All @@ -246,8 +246,8 @@ func (o *otlpOptions) AsMetricHTTP() []otlpmetrichttp.Option {
if o.SkipTLSVerify {
opts = append(opts, otlpmetrichttp.WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
}
if len(o.HTTPHeaders) > 0 {
opts = append(opts, otlpmetrichttp.WithHeaders(o.HTTPHeaders))
if len(o.Headers) > 0 {
opts = append(opts, otlpmetrichttp.WithHeaders(o.Headers))
}
return opts
}
Expand All @@ -262,8 +262,8 @@ func (o *otlpOptions) AsMetricGRPC() []otlpmetricgrpc.Option {
if o.SkipTLSVerify {
opts = append(opts, otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})))
}
if len(o.GRPCHeaders) > 0 {
opts = append(opts, otlpmetricgrpc.WithHeaders(o.GRPCHeaders))
if len(o.Headers) > 0 {
opts = append(opts, otlpmetricgrpc.WithHeaders(o.Headers))
}
return opts
}
Expand All @@ -281,8 +281,8 @@ func (o *otlpOptions) AsTraceHTTP() []otlptracehttp.Option {
if o.SkipTLSVerify {
opts = append(opts, otlptracehttp.WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true}))
}
if len(o.HTTPHeaders) > 0 {
opts = append(opts, otlptracehttp.WithHeaders(o.HTTPHeaders))
if len(o.Headers) > 0 {
opts = append(opts, otlptracehttp.WithHeaders(o.Headers))
}
return opts
}
Expand All @@ -297,8 +297,8 @@ func (o *otlpOptions) AsTraceGRPC() []otlptracegrpc.Option {
if o.SkipTLSVerify {
opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})))
}
if len(o.GRPCHeaders) > 0 {
opts = append(opts, otlptracegrpc.WithHeaders(o.GRPCHeaders))
if len(o.Headers) > 0 {
opts = append(opts, otlptracegrpc.WithHeaders(o.Headers))
}
return opts
}
Expand Down Expand Up @@ -359,7 +359,7 @@ func (l *LogrAdaptor) WithName(name string) logr.LogSink {
return &LogrAdaptor{inner: l.inner.With("name", name)}
}

func headersFromEnv(varName string) map[string]string {
func HeadersFromEnv(varName string) map[string]string {
headers := map[string]string{}

addToMap := func(k string, v string) {
Expand Down Expand Up @@ -404,3 +404,19 @@ func ResourceAttrsFromEnv(svc *svc.Attrs) []attribute.KeyValue {
parseOTELEnvVar(svc, envResourceAttrs, apply)
return otelResourceAttrs
}

func ResolveOTLPEndpoint(endpoint, common string, grafana *GrafanaOTLP) (string, bool) {
if endpoint != "" {
return endpoint, false
}

if common != "" {
return common, true
}

if grafana != nil && grafana.CloudZone != "" && grafana.Endpoint() != "" {
return grafana.Endpoint(), true
}

return "", false
}
42 changes: 42 additions & 0 deletions pkg/export/otel/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,45 @@ func TestParseOTELEnvVar_nil(t *testing.T) {

assert.True(t, reflect.DeepEqual(actual, map[string]string{}))
}

func TestResolveOTLPEndpoint(t *testing.T) {
grafana1 := GrafanaOTLP{
CloudZone: "foo",
}

const grafanaEndpoint = "https://otlp-gateway-foo.grafana.net/otlp"

grafana2 := GrafanaOTLP{}

type expected struct {
e string
common bool
}

type testCase struct {
endpoint string
common string
grafana *GrafanaOTLP
expected expected
}

testCases := []testCase{
{endpoint: "e1", common: "c1", grafana: nil, expected: expected{e: "e1", common: false}},
{endpoint: "e1", common: "", grafana: nil, expected: expected{e: "e1", common: false}},
{endpoint: "", common: "c1", grafana: nil, expected: expected{e: "c1", common: true}},
{endpoint: "", common: "", grafana: nil, expected: expected{e: "", common: false}},
{endpoint: "e1", common: "c1", grafana: &grafana1, expected: expected{e: "e1", common: false}},
{endpoint: "", common: "c1", grafana: &grafana1, expected: expected{e: "c1", common: true}},
{endpoint: "", common: "", grafana: &grafana1, expected: expected{e: grafanaEndpoint, common: true}},
{endpoint: "", common: "", grafana: &grafana2, expected: expected{e: "", common: false}},
}

for _, tc := range testCases {
t.Run(fmt.Sprint(tc), func(t *testing.T) {
ep, common := ResolveOTLPEndpoint(tc.endpoint, tc.common, tc.grafana)

assert.Equal(t, ep, tc.expected.e)
assert.Equal(t, common, tc.expected.common)
})
}
}
16 changes: 8 additions & 8 deletions pkg/export/otel/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,18 @@ func (cfg *GrafanaOTLP) AuthHeader() string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(cfg.InstanceID+":"+cfg.APIKey))
}

func (cfg *GrafanaOTLP) HasAuth() bool {
return cfg.InstanceID != "" && cfg.APIKey != ""
}

func (cfg *GrafanaOTLP) setupOptions(opt *otlpOptions) {
if cfg == nil {
return
}
if cfg.InstanceID != "" && cfg.APIKey != "" {
if opt.HTTPHeaders == nil {
opt.HTTPHeaders = map[string]string{}
}
opt.HTTPHeaders["Authorization"] = cfg.AuthHeader()
if opt.GRPCHeaders == nil {
opt.GRPCHeaders = map[string]string{}
if cfg.HasAuth() {
if opt.Headers == nil {
opt.Headers = map[string]string{}
}
opt.GRPCHeaders["Authorization"] = cfg.AuthHeader()
opt.Headers["Authorization"] = cfg.AuthHeader()
}
}
26 changes: 15 additions & 11 deletions pkg/export/otel/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"maps"
"net/url"
"os"
"slices"
Expand Down Expand Up @@ -144,6 +145,10 @@ func (m *MetricsConfig) GuessProtocol() Protocol {
return ProtocolHTTPProtobuf
}

func (m *MetricsConfig) OTLPMetricsEndpoint() (string, bool) {
return ResolveOTLPEndpoint(m.MetricsEndpoint, m.CommonEndpoint, m.Grafana)
}

// EndpointEnabled specifies that the OTEL metrics node is enabled if and only if
// either the OTEL endpoint and OTEL metrics endpoint is defined.
// If not enabled, this node won't be instantiated
Expand Down Expand Up @@ -963,7 +968,7 @@ func (mr *MetricsReporter) reportMetrics(input <-chan []request.Span) {
}

func getHTTPMetricEndpointOptions(cfg *MetricsConfig) (otlpOptions, error) {
opts := otlpOptions{}
opts := otlpOptions{Headers: map[string]string{}}
log := mlog().With("transport", "http")
murl, isCommon, err := parseMetricsEndpoint(cfg)
if err != nil {
Expand Down Expand Up @@ -996,12 +1001,14 @@ func getHTTPMetricEndpointOptions(cfg *MetricsConfig) (otlpOptions, error) {
}

cfg.Grafana.setupOptions(&opts)
maps.Copy(opts.Headers, HeadersFromEnv(envHeaders))
maps.Copy(opts.Headers, HeadersFromEnv(envMetricsHeaders))

return opts, nil
}

func getGRPCMetricEndpointOptions(cfg *MetricsConfig) (otlpOptions, error) {
opts := otlpOptions{}
opts := otlpOptions{Headers: map[string]string{}}
log := mlog().With("transport", "grpc")
murl, _, err := parseMetricsEndpoint(cfg)
if err != nil {
Expand All @@ -1020,6 +1027,11 @@ func getGRPCMetricEndpointOptions(cfg *MetricsConfig) (otlpOptions, error) {
log.Debug("Setting InsecureSkipVerify")
opts.SkipTLSVerify = true
}

cfg.Grafana.setupOptions(&opts)
maps.Copy(opts.Headers, HeadersFromEnv(envHeaders))
maps.Copy(opts.Headers, HeadersFromEnv(envMetricsHeaders))

return opts, nil
}

Expand All @@ -1030,15 +1042,7 @@ func getGRPCMetricEndpointOptions(cfg *MetricsConfig) (otlpOptions, error) {
// If, by some reason, Grafana changes its OTLP Gateway URL in a distant future, you can still point to the
// correct URL with the OTLP_EXPORTER_... variables.
func parseMetricsEndpoint(cfg *MetricsConfig) (*url.URL, bool, error) {
isCommon := false
endpoint := cfg.MetricsEndpoint
if endpoint == "" {
isCommon = true
endpoint = cfg.CommonEndpoint
if endpoint == "" && cfg.Grafana != nil && cfg.Grafana.CloudZone != "" {
endpoint = cfg.Grafana.Endpoint()
}
}
endpoint, isCommon := cfg.OTLPMetricsEndpoint()

murl, err := url.Parse(endpoint)
if err != nil {
Expand Down
Loading
Loading