From ff2f6493cec809e77bffac65d21a4342eb2dc187 Mon Sep 17 00:00:00 2001 From: Jack Shirazi Date: Wed, 15 Jul 2026 14:45:50 +0100 Subject: [PATCH 1/3] Add Elastic Otel Agent --- README.md | 1 + docs/framework-elastic_otel_java_agent.md | 76 ++++++ docs/framework-ordering.md | 3 +- manifest.yml | 16 ++ src/integration/frameworks_test.go | 39 +++ .../frameworks/elastic_otel_java_agent.go | 248 ++++++++++++++++++ .../elastic_otel_java_agent_test.go | 244 +++++++++++++++++ src/java/frameworks/framework.go | 1 + src/java/frameworks/java_opts_writer.go | 1 + 9 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 docs/framework-elastic_otel_java_agent.md create mode 100644 src/java/frameworks/elastic_otel_java_agent.go create mode 100644 src/java/frameworks/elastic_otel_java_agent_test.go diff --git a/README.md b/README.md index 489bd72cf5..609f29c31a 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ For historical analysis documents from development sessions, see [`docs/archive/ * [DataDog](docs/framework-datadog_javaagent.md) ([Configuration](docs/framework-datadog_javaagent.md#configuration) * [Debug](docs/framework-debug.md) ([Configuration](docs/framework-debug.md#configuration)) * [Elastic APM Agent](docs/framework-elastic_apm_agent.md) ([Configuration](docs/framework-elastic_apm_agent.md#configuration)) + * [Elastic OTel Java Agent](docs/framework-elastic_otel_java_agent.md) ([Configuration](docs/framework-elastic_otel_java_agent.md#configuration)) * [Dynatrace SaaS/Managed OneAgent](docs/framework-dynatrace_one_agent.md) ([Configuration](docs/framework-dynatrace_one_agent.md#configuration)) * [Google Stackdriver Profiler](docs/framework-google_stackdriver_profiler.md) ([Configuration](docs/framework-google_stackdriver_profiler.md#configuration)) * [Introscope Agent](docs/framework-introscope_agent.md) ([Configuration](docs/framework-introscope_agent.md#configuration)) diff --git a/docs/framework-elastic_otel_java_agent.md b/docs/framework-elastic_otel_java_agent.md new file mode 100644 index 0000000000..79100a12a1 --- /dev/null +++ b/docs/framework-elastic_otel_java_agent.md @@ -0,0 +1,76 @@ +# Elastic OTel Java Agent Framework + +The Elastic OTel Java Agent Framework causes an application to be automatically configured to work with the [Elastic Distribution of OpenTelemetry Java][]. + + + + + + + + + + +
Detection CriterionExistence of a bound Elastic OTel service. The service must have a name, label, or tag containing elastic-otel, edot-java, or elastic-edot, and must provide an OTLP endpoint plus authentication credentials. The framework can also be enabled explicitly with ELASTIC_OTEL_AGENT.
Tagselastic-otel-javaagent=<version>
+ +Tags are printed to standard output by the buildpack detect script. + +## User-Provided Service + +Users can provide their own Elastic OTel service. A user-provided service must have a name or tag with `elastic-otel`, `edot-java`, or `elastic-edot` in it so that the framework configures the application with the EDOT Java agent. + +The credential payload can contain the following entries. + +| Name | Description | +| ---- | ----------- | +| `otel.exporter.otlp.endpoint` | The OTLP endpoint for your Elastic deployment or EDOT Collector. Aliases: `otlp_endpoint`, `otlpEndpoint`, `endpoint`. | +| `otel.exporter.otlp.headers` | Explicit OTLP headers, for example `Authorization=ApiKey `. If present, this takes precedence over derived authentication headers. | +| `api_key` | Elastic API key. Converted to `otel.exporter.otlp.headers=Authorization=ApiKey `. | +| `secret_token` | Bearer token. Converted to `otel.exporter.otlp.headers=Authorization=Bearer `. | +| `access_token` | Bearer token. Converted to `otel.exporter.otlp.headers=Authorization=Bearer `. | +| `otel.*` | Any additional OpenTelemetry configuration applied as JVM system properties. | +| `elastic.otel.*` | Any additional Elastic OTel configuration applied as JVM system properties. | + +### Creating an Elastic OTel User-Provided Service + +Example minimal configuration: + +``` +cf cups my-elastic-otel-service -p '{"otel.exporter.otlp.endpoint":"https://my-deployment.ingest.us-west1.gcp.cloud.es.io","api_key":"my-api-key"}' +``` + +Example configuration with explicit OTLP headers and resource attributes: + +``` +cf cups my-elastic-otel-service -p '{"otel.exporter.otlp.endpoint":"https://my-deployment.ingest.us-west1.gcp.cloud.es.io","otel.exporter.otlp.headers":"Authorization=ApiKey my-api-key","otel.resource.attributes":"deployment.environment.name=production"}' +``` + +Bind your application to the service using: + +`cf bind-service my-app-name my-elastic-otel-service` + +or use the `services` block in the application manifest file. + +## Configuration + +For general information on configuring the buildpack, including how to specify configuration values through environment variables, refer to [Configuration and Extension][]. + +The framework uses the dependency version configured in `manifest.yml`. + +| Name | Description | +| ---- | ----------- | +| `version` | The version of Elastic OTel Java agent to use. The current buildpack manifest uses the `elastic-otel-javaagent` dependency. | + +The framework sets `otel.service.name` from `VCAP_APPLICATION.application_name` if it is not configured in service credentials or `OTEL_SERVICE_NAME`. It also sets `deployment.environment.name` from `VCAP_APPLICATION.space_name` when `otel.resource.attributes` is not configured. + +> **Warning** +> Do not bind this framework alongside the Elastic APM Agent, OpenTelemetry Javaagent, Splunk OTel Java Agent, or another Java agent framework for the same application. Running multiple Java agents in the same JVM can cause duplicate telemetry or conflicting instrumentation. + +## Migration From Elastic APM Agent + +The Elastic OTel Java agent uses OTLP configuration. It does not use Elastic APM agent settings such as `server_url` or `secret_token` with the `elastic.apm.*` prefix. + +When migrating, create a new service binding named or tagged for Elastic OTel and provide an OTLP endpoint plus `otel.exporter.otlp.headers` or `api_key`. + +[Configuration and Extension]: ../README.md#configuration-and-extension +[Elastic Distribution of OpenTelemetry Java]: https://www.elastic.co/docs/reference/opentelemetry/edot-sdks/java diff --git a/docs/framework-ordering.md b/docs/framework-ordering.md index d6158fffbe..0ae902d02d 100644 --- a/docs/framework-ordering.md +++ b/docs/framework-ordering.md @@ -50,7 +50,8 @@ Line | Framework Name | Priority | Notes 75 | SpringAutoReconfiguration | 41 | Spring framework 76 | SplunkOtelJavaAgent | 42 | Observability agent 77 | SpringInsight | 43 | Spring monitoring -78 | SkyWalkingAgent | 44 | APM agent +78 | SkyWalkingAgent | 41 | APM agent +78a | ElasticOtelJavaAgent | 44 | Observability agent 79 | YourKitProfiler | 45 | Profiler 80 | JavaSecurity | 47 | Security configuration 81 | JavaOpts | 99 | ⚠️ USER-DEFINED OPTS (ALWAYS LAST) diff --git a/manifest.yml b/manifest.yml index 75fd64e81a..0b09d875c8 100644 --- a/manifest.yml +++ b/manifest.yml @@ -21,6 +21,7 @@ packaging_profiles: exclude: - datadog-javaagent - elastic-apm-agent + - elastic-otel-javaagent - azure-application-insights - skywalking-agent - splunk-otel-javaagent @@ -44,6 +45,7 @@ packaging_profiles: exclude: - datadog-javaagent - elastic-apm-agent + - elastic-otel-javaagent - azure-application-insights - skywalking-agent - splunk-otel-javaagent @@ -94,6 +96,8 @@ default_versions: version: 1.x - name: elastic-apm-agent version: 1.x +- name: elastic-otel-javaagent + version: 1.x - name: azure-application-insights version: 3.x - name: skywalking-agent @@ -182,6 +186,9 @@ url_to_dependency_map: - match: elastic-apm-agent-(\d+\.\d+\.\d+) name: elastic-apm-agent version: "$1" +- match: elastic-otel-javaagent[-_](\d+\.\d+\.\d+) + name: elastic-otel-javaagent + version: "$1" - match: applicationinsights-agent-(\d+\.\d+\.\d+) name: azure-application-insights version: "$1" @@ -321,6 +328,15 @@ dependencies: - cflinuxfs5 source: https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/1.56.0/elastic-apm-agent-1.56.0.jar source_sha256: '' +- name: elastic-otel-javaagent + version: 1.11.0 + uri: https://repo1.maven.org/maven2/co/elastic/otel/elastic-otel-javaagent/1.11.0/elastic-otel-javaagent-1.11.0.jar + sha256: 61c0aae98e165ddfa3096d5d8ed16c159992b8ae284bf6296800839008cdb707 + cf_stacks: + - cflinuxfs4 + - cflinuxfs5 + source: https://repo1.maven.org/maven2/co/elastic/otel/elastic-otel-javaagent/1.11.0/elastic-otel-javaagent-1.11.0.jar + source_sha256: 61c0aae98e165ddfa3096d5d8ed16c159992b8ae284bf6296800839008cdb707 - name: google-stackdriver-profiler version: 0.4.0 uri: https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz diff --git a/src/integration/frameworks_test.go b/src/integration/frameworks_test.go index 1ae274d9bf..23a736f6a8 100644 --- a/src/integration/frameworks_test.go +++ b/src/integration/frameworks_test.go @@ -400,6 +400,45 @@ func testFrameworks(platform switchblade.Platform, fixtures string) func(*testin }) }) + context("with Elastic OTel service binding", func() { + it("detects and installs Elastic OTel Java agent", func() { + deployment, logs, err := platform.Deploy. + WithServices(map[string]switchblade.Service{ + "elastic-otel": { + "otel.exporter.otlp.endpoint": "https://elastic-otel.example.com:443", + "api_key": "test-api-key", + }, + }). + WithEnv(map[string]string{ + "BP_JAVA_VERSION": "17", + }). + Execute(name, filepath.Join(fixtures, "apps", "integration_valid")) + Expect(err).NotTo(HaveOccurred(), logs.String) + + Expect(logs.String()).To(ContainSubstring("Elastic OTel")) + Eventually(deployment).Should(matchers.Serve(ContainSubstring(""))) + }) + + it("configures Elastic OTel with explicit OTLP headers", func() { + deployment, logs, err := platform.Deploy. + WithServices(map[string]switchblade.Service{ + "my-elastic-otel": { + "otel.exporter.otlp.endpoint": "https://elastic-otel.production.example.com:443", + "otel.exporter.otlp.headers": "Authorization=ApiKey explicit-key", + "otel.service.name": "my-java-app", + }, + }). + WithEnv(map[string]string{ + "BP_JAVA_VERSION": "17", + }). + Execute(name, filepath.Join(fixtures, "containers", "spring_boot_staged")) + Expect(err).NotTo(HaveOccurred(), logs.String) + + Expect(logs.String()).To(ContainSubstring("Elastic OTel")) + Eventually(deployment).Should(matchers.Serve(ContainSubstring(""))) + }) + }) + context("with OpenTelemetry service binding", func() { it("detects and installs OpenTelemetry Javaagent", func() { deployment, logs, err := platform.Deploy. diff --git a/src/java/frameworks/elastic_otel_java_agent.go b/src/java/frameworks/elastic_otel_java_agent.go new file mode 100644 index 0000000000..b80fb9e2a8 --- /dev/null +++ b/src/java/frameworks/elastic_otel_java_agent.go @@ -0,0 +1,248 @@ +package frameworks + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/cloudfoundry/java-buildpack/src/java/common" +) + +// ElasticOtelJavaAgentFramework represents the Elastic Distribution of OpenTelemetry Java agent framework. +type ElasticOtelJavaAgentFramework struct { + context *common.Context + jarPath string + service *common.VCAPService +} + +// NewElasticOtelJavaAgentFramework creates a new Elastic OTel Java agent framework instance. +func NewElasticOtelJavaAgentFramework(ctx *common.Context) *ElasticOtelJavaAgentFramework { + return &ElasticOtelJavaAgentFramework{context: ctx} +} + +// Detect checks if the Elastic OTel Java agent should be enabled. +func (e *ElasticOtelJavaAgentFramework) Detect() (string, error) { + if os.Getenv("ELASTIC_OTEL_AGENT") != "" { + e.context.Log.Debug("Elastic OTel Java agent framework detected via ELASTIC_OTEL_AGENT") + return "elastic-otel-javaagent", nil + } + if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" && os.Getenv("OTEL_EXPORTER_OTLP_HEADERS") != "" { + e.context.Log.Debug("Elastic OTel Java agent framework detected via OTLP environment variables") + return "elastic-otel-javaagent", nil + } + + service := e.findElasticOtelService() + if service == nil { + e.context.Log.Debug("Elastic OTel Java agent: no elastic-otel service found") + return "", nil + } + if !e.hasRequiredCredentials(service) { + e.context.Log.Debug("Elastic OTel Java agent: service missing OTLP endpoint or authentication credentials") + return "", nil + } + + e.service = service + e.context.Log.Debug("Elastic OTel Java agent framework detected") + return "elastic-otel-javaagent", nil +} + +// Supply downloads and installs the Elastic OTel Java agent. +func (e *ElasticOtelJavaAgentFramework) Supply() error { + e.context.Log.Debug("Installing Elastic OTel Java agent") + + dep, err := e.context.Manifest.DefaultVersion(e.DependencyIdentifier()) + if err != nil { + return fmt.Errorf("unable to find Elastic OTel Java agent in manifest: %w", err) + } + + agentDir := filepath.Join(e.context.Stager.DepDir(), "elastic_otel_java_agent") + if err := e.context.Installer.InstallDependency(dep, agentDir); err != nil { + return fmt.Errorf("failed to install Elastic OTel Java agent: %w", err) + } + if err := e.constructJarPath(agentDir); err != nil { + return fmt.Errorf("elastic OTel Java agent JAR path not found during supply: %w", err) + } + + e.context.Log.Info("Elastic OTel Java agent %s installed", dep.Version) + return nil +} + +// Finalize configures the Elastic OTel Java agent. +func (e *ElasticOtelJavaAgentFramework) Finalize() error { + agentDir := filepath.Join(e.context.Stager.DepDir(), "elastic_otel_java_agent") + if err := e.constructJarPath(agentDir); err != nil { + return fmt.Errorf("elastic OTel Java agent JAR path not found during finalize: %w", err) + } + + e.context.Log.BeginStep("Configuring Elastic OTel Java agent") + + relPath, err := filepath.Rel(e.context.Stager.DepDir(), e.jarPath) + if err != nil { + return fmt.Errorf("failed to determine relative path for Elastic OTel Java agent: %w", err) + } + runtimeJarPath := filepath.Join(fmt.Sprintf("$DEPS_DIR/%s", e.context.Stager.DepsIdx()), relPath) + + config := e.buildConfiguration() + opts := []string{fmt.Sprintf("-javaagent:%s", runtimeJarPath)} + for key, value := range config { + opts = append(opts, formatJavaSystemProperty(key, value)) + } + + if err := writeJavaOptsFile(e.context, 44, "elastic_otel_java_agent", strings.Join(opts, " ")); err != nil { + return fmt.Errorf("failed to write JAVA_OPTS for Elastic OTel Java agent: %w", err) + } + + e.context.Log.Debug("Elastic OTel Java agent configured") + return nil +} + +func (e *ElasticOtelJavaAgentFramework) findElasticOtelService() *common.VCAPService { + vcapServices, err := GetVCAPServices() + if err != nil { + e.context.Log.Warning("Failed to parse VCAP_SERVICES: %s", err.Error()) + return nil + } + + for _, label := range []string{"elastic-otel", "edot-java", "elastic-edot"} { + if service := vcapServices.GetService(label); service != nil { + return service + } + } + for _, services := range vcapServices { + for _, service := range services { + if hasAnyTag(service, "elastic-otel", "edot-java", "elastic-edot") || + common.ContainsIgnoreCase(service.Name, "elastic-otel") || + common.ContainsIgnoreCase(service.Name, "edot-java") || + common.ContainsIgnoreCase(service.Name, "elastic-edot") { + return &service + } + } + } + + return nil +} + +func (e *ElasticOtelJavaAgentFramework) hasRequiredCredentials(service *common.VCAPService) bool { + if service == nil || service.Credentials == nil { + return false + } + return getOtlpEndpoint(service.Credentials) != "" && getOtlpHeaders(service.Credentials) != "" +} + +func (e *ElasticOtelJavaAgentFramework) buildConfiguration() map[string]string { + config := map[string]string{} + if service := e.findElasticOtelService(); service != nil { + e.service = service + for key, value := range service.Credentials { + strValue, ok := value.(string) + if !ok || strValue == "" { + continue + } + if strings.HasPrefix(key, "otel.") || strings.HasPrefix(key, "elastic.otel.") { + config[key] = strValue + } + } + if endpoint := getOtlpEndpoint(service.Credentials); endpoint != "" { + config["otel.exporter.otlp.endpoint"] = endpoint + } + if headers := getOtlpHeaders(service.Credentials); headers != "" { + config["otel.exporter.otlp.headers"] = headers + } + } + + if endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"); endpoint != "" { + config["otel.exporter.otlp.endpoint"] = endpoint + } + if headers := os.Getenv("OTEL_EXPORTER_OTLP_HEADERS"); headers != "" { + config["otel.exporter.otlp.headers"] = headers + } + if serviceName := os.Getenv("OTEL_SERVICE_NAME"); serviceName != "" { + config["otel.service.name"] = serviceName + } + if _, ok := config["otel.service.name"]; !ok { + if appName := GetApplicationName(false); appName != "" { + config["otel.service.name"] = appName + } + } + if _, ok := config["otel.resource.attributes"]; !ok { + if spaceName := getSpaceName(); spaceName != "" { + config["otel.resource.attributes"] = "deployment.environment.name=" + spaceName + } + } + + return config +} + +func getOtlpEndpoint(credentials map[string]interface{}) string { + for _, key := range []string{"otel.exporter.otlp.endpoint", "otlp_endpoint", "otlpEndpoint", "endpoint"} { + if value, ok := credentials[key].(string); ok && value != "" { + return value + } + } + return "" +} + +func getOtlpHeaders(credentials map[string]interface{}) string { + if value, ok := credentials["otel.exporter.otlp.headers"].(string); ok && value != "" { + return value + } + if value, ok := credentials["api_key"].(string); ok && value != "" { + return "Authorization=ApiKey " + value + } + if value, ok := credentials["secret_token"].(string); ok && value != "" { + return "Authorization=Bearer " + value + } + if value, ok := credentials["access_token"].(string); ok && value != "" { + return "Authorization=Bearer " + value + } + return "" +} + +func formatJavaSystemProperty(key, value string) string { + return fmt.Sprintf("-D%s=%s", key, shellEscape(value)) +} + +func hasAnyTag(service common.VCAPService, tags ...string) bool { + for _, actual := range service.Tags { + for _, expected := range tags { + if strings.EqualFold(actual, expected) { + return true + } + } + } + return false +} + +func getSpaceName() string { + vcapApp := os.Getenv("VCAP_APPLICATION") + if vcapApp == "" { + return "" + } + var appData map[string]interface{} + if err := json.Unmarshal([]byte(vcapApp), &appData); err != nil { + return "" + } + if spaceName, ok := appData["space_name"].(string); ok { + return spaceName + } + return "" +} + +func (e *ElasticOtelJavaAgentFramework) constructJarPath(agentDir string) error { + jarPattern := filepath.Join(agentDir, e.DependencyIdentifier()+"*.jar") + matches, err := filepath.Glob(jarPattern) + if err != nil { + return fmt.Errorf("failed to search for Elastic OTel javaagent jar: %w", err) + } + if len(matches) == 0 { + return fmt.Errorf("elastic OTel Java agent jar not found after installation in %s", agentDir) + } + e.jarPath = matches[0] + return nil +} + +func (e *ElasticOtelJavaAgentFramework) DependencyIdentifier() string { + return "elastic-otel-javaagent" +} diff --git a/src/java/frameworks/elastic_otel_java_agent_test.go b/src/java/frameworks/elastic_otel_java_agent_test.go new file mode 100644 index 0000000000..b08ba945a3 --- /dev/null +++ b/src/java/frameworks/elastic_otel_java_agent_test.go @@ -0,0 +1,244 @@ +package frameworks_test + +import ( + "os" + "path/filepath" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/java-buildpack/src/java/frameworks" + "github.com/cloudfoundry/libbuildpack" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ElasticOtelJavaAgentFramework", func() { + var ( + ctx *common.Context + framework *frameworks.ElasticOtelJavaAgentFramework + tmpDir string + depsDir string + ) + + BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "elastic-otel-javaagent-test-*") + Expect(err).NotTo(HaveOccurred()) + + depsDir = filepath.Join(tmpDir, "deps") + Expect(os.MkdirAll(filepath.Join(depsDir, "0"), 0755)).To(Succeed()) + + agentDir := filepath.Join(depsDir, "0", "elastic_otel_java_agent") + Expect(os.MkdirAll(agentDir, 0755)).To(Succeed()) + Expect(os.WriteFile( + filepath.Join(agentDir, "elastic-otel-javaagent-1.11.0.jar"), + []byte("fake jar"), 0644, + )).To(Succeed()) + + logger := libbuildpack.NewLogger(GinkgoWriter) + manifest := &libbuildpack.Manifest{} + stager := libbuildpack.NewStager([]string{tmpDir, "", depsDir, "0"}, logger, manifest) + + ctx = &common.Context{ + Stager: stager, + Manifest: manifest, + Log: logger, + } + framework = frameworks.NewElasticOtelJavaAgentFramework(ctx) + }) + + AfterEach(func() { + os.RemoveAll(tmpDir) + os.Unsetenv("VCAP_SERVICES") + os.Unsetenv("VCAP_APPLICATION") + os.Unsetenv("ELASTIC_OTEL_AGENT") + os.Unsetenv("OTEL_EXPORTER_OTLP_ENDPOINT") + os.Unsetenv("OTEL_EXPORTER_OTLP_HEADERS") + os.Unsetenv("OTEL_SERVICE_NAME") + }) + + Describe("Detect", func() { + It("does not detect without service binding or explicit environment", func() { + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(BeEmpty()) + }) + + It("detects an elastic-otel service with endpoint and api key", func() { + os.Setenv("VCAP_SERVICES", `{ + "elastic-otel": [{ + "name": "my-elastic-otel", + "label": "elastic-otel", + "tags": [], + "credentials": { + "otel.exporter.otlp.endpoint": "https://elastic.example.com:443", + "api_key": "abc123" + } + }] + }`) + + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("elastic-otel-javaagent")) + }) + + It("detects a user-provided service tagged edot-java", func() { + os.Setenv("VCAP_SERVICES", `{ + "user-provided": [{ + "name": "telemetry", + "label": "user-provided", + "tags": ["edot-java"], + "credentials": { + "endpoint": "https://elastic.example.com:443", + "otel.exporter.otlp.headers": "Authorization=ApiKey abc123" + } + }] + }`) + + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("elastic-otel-javaagent")) + }) + + It("detects a service name containing elastic-otel", func() { + os.Setenv("VCAP_SERVICES", `{ + "user-provided": [{ + "name": "prod-elastic-otel", + "label": "user-provided", + "tags": [], + "credentials": { + "otlp_endpoint": "https://elastic.example.com:443", + "secret_token": "secret" + } + }] + }`) + + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("elastic-otel-javaagent")) + }) + + It("detects via ELASTIC_OTEL_AGENT", func() { + os.Setenv("ELASTIC_OTEL_AGENT", "true") + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("elastic-otel-javaagent")) + }) + + It("detects via OTLP endpoint and headers environment variables", func() { + os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://elastic.example.com:443") + os.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=ApiKey abc123") + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(Equal("elastic-otel-javaagent")) + }) + + It("does not detect a generic OpenTelemetry collector service", func() { + os.Setenv("VCAP_SERVICES", `{ + "otel-collector": [{ + "name": "my-otel", + "label": "otel-collector", + "tags": ["otel"], + "credentials": { + "endpoint": "http://collector:4318" + } + }] + }`) + + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(BeEmpty()) + }) + + It("does not detect an Elastic APM service", func() { + os.Setenv("VCAP_SERVICES", `{ + "elastic-apm": [{ + "name": "my-elastic-apm", + "label": "elastic-apm", + "tags": ["elastic-apm"], + "credentials": { + "server_url": "https://apm.example.com:8200", + "secret_token": "secret" + } + }] + }`) + + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(BeEmpty()) + }) + + It("does not detect an elastic-otel service missing authentication", func() { + os.Setenv("VCAP_SERVICES", `{ + "elastic-otel": [{ + "name": "my-elastic-otel", + "label": "elastic-otel", + "tags": [], + "credentials": { + "endpoint": "https://elastic.example.com:443" + } + }] + }`) + + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(BeEmpty()) + }) + + It("does not detect on OTLP endpoint environment variable alone", func() { + os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://elastic.example.com:443") + name, err := framework.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(name).To(BeEmpty()) + }) + }) + + Describe("Finalize", func() { + optsFile := func() string { + return filepath.Join(depsDir, "0", "java_opts", "44_elastic_otel_java_agent.opts") + } + + It("writes javaagent, endpoint, derived API key header, and application service name", func() { + os.Setenv("VCAP_APPLICATION", `{"application_name":"my-cf-app","space_name":"production"}`) + os.Setenv("VCAP_SERVICES", `{ + "elastic-otel": [{ + "name": "my-elastic-otel", + "label": "elastic-otel", + "tags": [], + "credentials": { + "otel.exporter.otlp.endpoint": "https://elastic.example.com:443", + "api_key": "abc123", + "elastic.otel.javaagent.log.level": "DEBUG" + } + }] + }`) + + Expect(framework.Finalize()).To(Succeed()) + + data, err := os.ReadFile(optsFile()) + Expect(err).NotTo(HaveOccurred()) + opts := string(data) + Expect(opts).To(ContainSubstring("-javaagent:$DEPS_DIR/0/elastic_otel_java_agent/elastic-otel-javaagent-1.11.0.jar")) + Expect(opts).To(ContainSubstring("-Dotel.exporter.otlp.endpoint=https://elastic.example.com:443")) + Expect(opts).To(ContainSubstring("-Dotel.exporter.otlp.headers='Authorization=ApiKey abc123'")) + Expect(opts).To(ContainSubstring("-Delastic.otel.javaagent.log.level=DEBUG")) + Expect(opts).To(ContainSubstring("-Dotel.service.name=my-cf-app")) + Expect(opts).To(ContainSubstring("-Dotel.resource.attributes=deployment.environment.name=production")) + Expect(opts).NotTo(ContainSubstring(tmpDir)) + }) + + It("uses explicit OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_HEADERS environment values", func() { + os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://env.elastic.example.com:443") + os.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=ApiKey env-key") + os.Setenv("OTEL_SERVICE_NAME", "env-service") + + Expect(framework.Finalize()).To(Succeed()) + + data, err := os.ReadFile(optsFile()) + Expect(err).NotTo(HaveOccurred()) + opts := string(data) + Expect(opts).To(ContainSubstring("-Dotel.exporter.otlp.endpoint=https://env.elastic.example.com:443")) + Expect(opts).To(ContainSubstring("-Dotel.exporter.otlp.headers='Authorization=ApiKey env-key'")) + Expect(opts).To(ContainSubstring("-Dotel.service.name=env-service")) + }) + }) +}) diff --git a/src/java/frameworks/framework.go b/src/java/frameworks/framework.go index bf51010af8..e2157f7f79 100644 --- a/src/java/frameworks/framework.go +++ b/src/java/frameworks/framework.go @@ -96,6 +96,7 @@ func (r *Registry) RegisterStandardFrameworks() { r.Register(NewGoogleStackdriverProfilerFramework(r.context)) r.Register(NewIntroscopeAgentFramework(r.context)) r.Register(NewOpenTelemetryJavaagentFramework(r.context)) + r.Register(NewElasticOtelJavaAgentFramework(r.context)) r.Register(NewRiverbedAppInternalsAgentFramework(r.context)) r.Register(NewSkyWalkingAgentFramework(r.context)) r.Register(NewSplunkOtelJavaAgentFramework(r.context)) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index 8e0dcfcd58..562f9cadd5 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -35,6 +35,7 @@ import ( // - 40: Seeker Security Provider // - 41: SkyWalking Agent // - 42: Splunk OTEL Java Agent +// - 44: Elastic OTel Java Agent // - 45: YourKit Profiler // - 46: Takipi Agent // - 99: User JAVA_OPTS (always last) From dcbfda188c8d75b7d8c7448b15e4e8eac45a2f37 Mon Sep 17 00:00:00 2001 From: Jack Shirazi Date: Wed, 15 Jul 2026 15:00:01 +0100 Subject: [PATCH 2/3] typos --- docs/framework-elastic_otel_java_agent.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/framework-elastic_otel_java_agent.md b/docs/framework-elastic_otel_java_agent.md index 79100a12a1..3c1951b278 100644 --- a/docs/framework-elastic_otel_java_agent.md +++ b/docs/framework-elastic_otel_java_agent.md @@ -5,7 +5,7 @@ The Elastic OTel Java Agent Framework causes an application to be automatically - + @@ -17,7 +17,7 @@ Tags are printed to standard output by the buildpack detect script. ## User-Provided Service -Users can provide their own Elastic OTel service. A user-provided service must have a name or tag with `elastic-otel`, `edot-java`, or `elastic-edot` in it so that the framework configures the application with the EDOT Java agent. +Users can provide their own Elastic OTel service. A user-provided service must have a name or tag with `elastic-otel` in it so that the framework configures the application with the Elastic Otel Java agent. The credential payload can contain the following entries. @@ -27,7 +27,6 @@ The credential payload can contain the following entries. | `otel.exporter.otlp.headers` | Explicit OTLP headers, for example `Authorization=ApiKey `. If present, this takes precedence over derived authentication headers. | | `api_key` | Elastic API key. Converted to `otel.exporter.otlp.headers=Authorization=ApiKey `. | | `secret_token` | Bearer token. Converted to `otel.exporter.otlp.headers=Authorization=Bearer `. | -| `access_token` | Bearer token. Converted to `otel.exporter.otlp.headers=Authorization=Bearer `. | | `otel.*` | Any additional OpenTelemetry configuration applied as JVM system properties. | | `elastic.otel.*` | Any additional Elastic OTel configuration applied as JVM system properties. | From 61ad9ae381aab3da268c2032b955de26320332a0 Mon Sep 17 00:00:00 2001 From: Jack Shirazi Date: Wed, 15 Jul 2026 15:12:31 +0100 Subject: [PATCH 3/3] licenses --- src/java/frameworks/elastic_otel_java_agent.go | 15 +++++++++++++++ .../frameworks/elastic_otel_java_agent_test.go | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/java/frameworks/elastic_otel_java_agent.go b/src/java/frameworks/elastic_otel_java_agent.go index b80fb9e2a8..6acd1f8ebf 100644 --- a/src/java/frameworks/elastic_otel_java_agent.go +++ b/src/java/frameworks/elastic_otel_java_agent.go @@ -1,3 +1,18 @@ +// Cloud Foundry Java Buildpack +// Copyright 2013-2026 the original author or authors. +// +// 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. + package frameworks import ( diff --git a/src/java/frameworks/elastic_otel_java_agent_test.go b/src/java/frameworks/elastic_otel_java_agent_test.go index b08ba945a3..981cf82a9a 100644 --- a/src/java/frameworks/elastic_otel_java_agent_test.go +++ b/src/java/frameworks/elastic_otel_java_agent_test.go @@ -1,3 +1,18 @@ +// Cloud Foundry Java Buildpack +// Copyright 2013-2026 the original author or authors. +// +// 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. + package frameworks_test import (
Detection CriterionExistence of a bound Elastic OTel service. The service must have a name, label, or tag containing elastic-otel, edot-java, or elastic-edot, and must provide an OTLP endpoint plus authentication credentials. The framework can also be enabled explicitly with ELASTIC_OTEL_AGENT.Existence of a bound Elastic OTel service. The service must have a name, label, or tag containing elastic-otel and must provide an OTLP endpoint plus authentication credentials. The framework can also be enabled explicitly with ELASTIC_OTEL_AGENT.
Tags