diff --git a/README.md b/README.md
index 489bd72cf..609f29c31 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 000000000..3c1951b27
--- /dev/null
+++ b/docs/framework-elastic_otel_java_agent.md
@@ -0,0 +1,75 @@
+# 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 Criterion |
+ 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 |
+ elastic-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` in it so that the framework configures the application with the Elastic Otel 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 `. |
+| `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 d6158fffb..0ae902d02 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 75fd64e81..0b09d875c 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 1ae274d9b..23a736f6a 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 000000000..6acd1f8eb
--- /dev/null
+++ b/src/java/frameworks/elastic_otel_java_agent.go
@@ -0,0 +1,263 @@
+// 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 (
+ "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 000000000..981cf82a9
--- /dev/null
+++ b/src/java/frameworks/elastic_otel_java_agent_test.go
@@ -0,0 +1,259 @@
+// 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 (
+ "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 bf51010af..e2157f7f7 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 8e0dcfcd5..562f9cadd 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)