From a1fe6b37147c7c0fe7d17687a1834d97587313d4 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 14 Jan 2026 09:42:44 +0100 Subject: [PATCH 01/18] added cf-metrics-exporter agent --- docs/framework-cf_metrics_exporter.md | 40 +++++++++ manifest.yml | 8 ++ src/java/frameworks/cf_metrics_exporter.go | 89 +++++++++++++++++++ .../frameworks/cf_metrics_exporter_test.go | 59 ++++++++++++ src/java/frameworks/framework.go | 2 + 5 files changed, 198 insertions(+) create mode 100644 docs/framework-cf_metrics_exporter.md create mode 100644 src/java/frameworks/cf_metrics_exporter.go create mode 100644 src/java/frameworks/cf_metrics_exporter_test.go diff --git a/docs/framework-cf_metrics_exporter.md b/docs/framework-cf_metrics_exporter.md new file mode 100644 index 000000000..e731ac223 --- /dev/null +++ b/docs/framework-cf_metrics_exporter.md @@ -0,0 +1,40 @@ +# cf-metrics-exporter (Agent Mode) + +This framework integrates the [cf-metrics-exporter](https://github.com/rabobank/cf-metrics-exporter) as a Java agent in the Java buildpack. + +## Enabling the Exporter + +Set the following environment variable to enable the agent: + +``` +CF_METRICS_EXPORTER_ENABLED=true +``` + +## Configuration + +- **CF_METRICS_EXPORTER_ENABLED**: Set to `true` to enable the agent (default: disabled). +- **CF_METRICS_EXPORTER_PROPS**: (Optional) Properties string to pass to the agent, e.g. `port=9090,foo=bar`. + +## How it Works + +- The agent JAR is downloaded during the buildpack supply phase. +- The agent is injected into the JVM at runtime using the `-javaagent` option. +- If `CF_METRICS_EXPORTER_PROPS` is set, its value is appended to the `-javaagent` option. + +## Example + +``` +CF_METRICS_EXPORTER_ENABLED=true +CF_METRICS_EXPORTER_PROPS="port=9090,foo=bar" +``` + +## Version + +- Default version: 0.7.1 +- Default download URI: https://github.com/rabobank/cf-metrics-exporter/releases/download/0.7.1/cf-metrics-exporter-0.7.1.jar + +## Notes + +- The agent is injected with priority 43 in JAVA_OPTS (after other APM agents). +- The agent JAR is placed in `.java-buildpack/cf_metrics_exporter/` within the dependency directory. + diff --git a/manifest.yml b/manifest.yml index 481f13d77..abf3ad633 100644 --- a/manifest.yml +++ b/manifest.yml @@ -537,6 +537,14 @@ dependencies: cf_stacks: - cflinuxfs4 +# cf-metrics-exporter Agent +- name: cf_metrics_exporter + version: 0.7.1 + uri: https://github.com/rabobank/cf-metrics-exporter/releases/download/0.7.1/cf-metrics-exporter-0.7.1.jar + sha256: 7ebabd3ffd812082cf92a513c8d2ac52906f5b42cd952cbe740bd5d5b086e79b + cf_stacks: + - cflinuxfs4 + # Container Security Provider # Note: Always enabled by default, provides container-specific security context - name: container-security-provider diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go new file mode 100644 index 000000000..33093a339 --- /dev/null +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -0,0 +1,89 @@ +package frameworks + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/libbuildpack" +) + +const cfMetricsExporterDependencyName = "cf_metrics_exporter" + +type CfMetricsExporterFramework struct { + ctx *common.Context +} + +func NewCfMetricsExporterFramework(ctx *common.Context) *CfMetricsExporterFramework { + return &CfMetricsExporterFramework{ctx: ctx} +} + +func (f *CfMetricsExporterFramework) Detect() (string, error) { + enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") + if enabled == "true" || enabled == "TRUE" { + version, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) + if err != nil { + return "cf-metrics-exporter", nil // fallback if version not found + } + return fmt.Sprintf("cf-metrics-exporter=%s", version), nil + } + return "", nil +} + +func (f *CfMetricsExporterFramework) getManifestDependency() (libbuildpack.Dependency, *libbuildpack.ManifestEntry, error) { + dep, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) + if err != nil { + return libbuildpack.Dependency{}, nil, fmt.Errorf("cf_metrics_exporter version not found in manifest: %w", err) + } + entry, err := f.ctx.Manifest.GetEntry(dep) + if err != nil { + return dep, nil, fmt.Errorf("cf_metrics_exporter manifest entry not found: %w", err) + } + return dep, entry, nil +} + +func (f *CfMetricsExporterFramework) Supply() error { + enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") + if enabled != "true" && enabled != "TRUE" { + return nil + } + dep, _, err := f.getManifestDependency() + if err != nil { + return err + } + agentDir := filepath.Join(f.ctx.Stager.DepDir(), ".java-buildpack", "cf_metrics_exporter") + if err := os.MkdirAll(agentDir, 0755); err != nil { + return fmt.Errorf("failed to create agent dir: %w", err) + } + jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) + agentPath := filepath.Join(agentDir, jarName) + if _, err := os.Stat(agentPath); os.IsNotExist(err) { + if err := f.ctx.Installer.InstallDependency(dep, agentPath); err != nil { + return fmt.Errorf("failed to download cf-metrics-exporter: %w", err) + } + } + return nil +} + +func (f *CfMetricsExporterFramework) Finalize() error { + enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") + if enabled != "true" && enabled != "TRUE" { + return nil + } + dep, _, err := f.getManifestDependency() + if err != nil { + return err + } + jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) + agentPath := filepath.Join(".java-buildpack", "cf_metrics_exporter", jarName) + props := os.Getenv("CF_METRICS_EXPORTER_PROPS") + var javaOpt string + if props != "" { + javaOpt = fmt.Sprintf("-javaagent:%s=%s", agentPath, props) + } else { + javaOpt = fmt.Sprintf("-javaagent:%s", agentPath) + } + // Priority 43: after SkyWalking (41), Splunk OTEL (42) + return writeJavaOptsFile(f.ctx, 43, "cf_metrics_exporter", javaOpt) +} diff --git a/src/java/frameworks/cf_metrics_exporter_test.go b/src/java/frameworks/cf_metrics_exporter_test.go new file mode 100644 index 000000000..e0677b164 --- /dev/null +++ b/src/java/frameworks/cf_metrics_exporter_test.go @@ -0,0 +1,59 @@ +package frameworks + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/libbuildpack" +) + +func TestDetectEnabledWithRealManifest(t *testing.T) { + if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "true"); err != nil { + t.Fatalf("Setenv failed: %v", err) + } + manifestDir := filepath.Join("../../../") + logger := libbuildpack.NewLogger(os.Stdout) + manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) + if err != nil { + t.Fatalf("Failed to load manifest.yml: %v", err) + } + ctx := &common.Context{Manifest: manifest} + f := NewCfMetricsExporterFramework(ctx) + name, err := f.Detect() + if err != nil { + t.Fatalf("Detect() error: %v", err) + } + if name == "" { + t.Error("Detect() should return non-empty name when enabled") + } + if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { + t.Fatalf("Unsetenv failed: %v", err) + } +} + +func TestDetectDisabledWithRealManifest(t *testing.T) { + if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "false"); err != nil { + t.Fatalf("Setenv failed: %v", err) + } + manifestDir := filepath.Join("../../../") + logger := libbuildpack.NewLogger(os.Stdout) + manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) + if err != nil { + t.Fatalf("Failed to load manifest.yml: %v", err) + } + ctx := &common.Context{Manifest: manifest} + f := NewCfMetricsExporterFramework(ctx) + name, err := f.Detect() + if err != nil { + t.Fatalf("Detect() error: %v", err) + } + if name != "" { + t.Error("Detect() should return empty name when disabled") + } + if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { + t.Fatalf("Unsetenv failed: %v", err) + } +} diff --git a/src/java/frameworks/framework.go b/src/java/frameworks/framework.go index a90697150..55968e3ad 100644 --- a/src/java/frameworks/framework.go +++ b/src/java/frameworks/framework.go @@ -74,6 +74,8 @@ func (r *Registry) RegisterStandardFrameworks() { // Metrics & Observability (Priority 1) r.Register(NewMetricWriterFramework(r.context)) + // Register cf-metrics-exporter agent (agent mode) + r.Register(NewCfMetricsExporterFramework(r.context)) // Development Tools (Priority 1) r.Register(NewDebugFramework(r.context)) From 5c6f9051592f852d927d8ef118571c9e72f037bb Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 14 Jan 2026 14:26:02 +0100 Subject: [PATCH 02/18] add default version for cf-metrics-exporter --- manifest.yml | 4 +++- src/java/frameworks/cf_metrics_exporter.go | 15 ++++++++------- src/java/frameworks/cf_metrics_exporter_test.go | 7 +++++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/manifest.yml b/manifest.yml index abf3ad633..c63636c72 100644 --- a/manifest.yml +++ b/manifest.yml @@ -80,6 +80,8 @@ default_versions: version: 7.x - name: newrelic version: 8.x +- name: cf-metrics-exporter + version: 0.7.x url_to_dependency_map: - match: openjdk-jre-(\d+\.\d+\.\d+) @@ -538,7 +540,7 @@ dependencies: - cflinuxfs4 # cf-metrics-exporter Agent -- name: cf_metrics_exporter +- name: cf-metrics-exporter version: 0.7.1 uri: https://github.com/rabobank/cf-metrics-exporter/releases/download/0.7.1/cf-metrics-exporter-0.7.1.jar sha256: 7ebabd3ffd812082cf92a513c8d2ac52906f5b42cd952cbe740bd5d5b086e79b diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 33093a339..98ffb74ec 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -9,7 +9,8 @@ import ( "github.com/cloudfoundry/libbuildpack" ) -const cfMetricsExporterDependencyName = "cf_metrics_exporter" +const cfMetricsExporterDependencyName = "cf-metrics-exporter" +const cfMetricsExporterDirName = "cf_metrics_exporter" type CfMetricsExporterFramework struct { ctx *common.Context @@ -24,7 +25,7 @@ func (f *CfMetricsExporterFramework) Detect() (string, error) { if enabled == "true" || enabled == "TRUE" { version, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { - return "cf-metrics-exporter", nil // fallback if version not found + return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } return fmt.Sprintf("cf-metrics-exporter=%s", version), nil } @@ -34,11 +35,11 @@ func (f *CfMetricsExporterFramework) Detect() (string, error) { func (f *CfMetricsExporterFramework) getManifestDependency() (libbuildpack.Dependency, *libbuildpack.ManifestEntry, error) { dep, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { - return libbuildpack.Dependency{}, nil, fmt.Errorf("cf_metrics_exporter version not found in manifest: %w", err) + return libbuildpack.Dependency{}, nil, fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } entry, err := f.ctx.Manifest.GetEntry(dep) if err != nil { - return dep, nil, fmt.Errorf("cf_metrics_exporter manifest entry not found: %w", err) + return dep, nil, fmt.Errorf("cf-metrics-exporter manifest entry not found: %w", err) } return dep, entry, nil } @@ -52,7 +53,7 @@ func (f *CfMetricsExporterFramework) Supply() error { if err != nil { return err } - agentDir := filepath.Join(f.ctx.Stager.DepDir(), ".java-buildpack", "cf_metrics_exporter") + agentDir := filepath.Join(f.ctx.Stager.DepDir(), ".java-buildpack", cfMetricsExporterDirName) if err := os.MkdirAll(agentDir, 0755); err != nil { return fmt.Errorf("failed to create agent dir: %w", err) } @@ -76,7 +77,7 @@ func (f *CfMetricsExporterFramework) Finalize() error { return err } jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) - agentPath := filepath.Join(".java-buildpack", "cf_metrics_exporter", jarName) + agentPath := filepath.Join(".java-buildpack", cfMetricsExporterDirName, jarName) props := os.Getenv("CF_METRICS_EXPORTER_PROPS") var javaOpt string if props != "" { @@ -85,5 +86,5 @@ func (f *CfMetricsExporterFramework) Finalize() error { javaOpt = fmt.Sprintf("-javaagent:%s", agentPath) } // Priority 43: after SkyWalking (41), Splunk OTEL (42) - return writeJavaOptsFile(f.ctx, 43, "cf_metrics_exporter", javaOpt) + return writeJavaOptsFile(f.ctx, 43, cfMetricsExporterDirName, javaOpt) } diff --git a/src/java/frameworks/cf_metrics_exporter_test.go b/src/java/frameworks/cf_metrics_exporter_test.go index e0677b164..6e62720a9 100644 --- a/src/java/frameworks/cf_metrics_exporter_test.go +++ b/src/java/frameworks/cf_metrics_exporter_test.go @@ -14,6 +14,10 @@ func TestDetectEnabledWithRealManifest(t *testing.T) { if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "true"); err != nil { t.Fatalf("Setenv failed: %v", err) } + // needed to match the cf-metrics-exporter dependency in the manifest + if err := os.Setenv("CF_STACK", "cflinuxfs4"); err != nil { + t.Fatalf("Setenv failed: %v", err) + } manifestDir := filepath.Join("../../../") logger := libbuildpack.NewLogger(os.Stdout) manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) @@ -29,6 +33,9 @@ func TestDetectEnabledWithRealManifest(t *testing.T) { if name == "" { t.Error("Detect() should return non-empty name when enabled") } + if err := os.Unsetenv("CF_STACK"); err != nil { + t.Fatalf("Unsetenv failed: %v", err) + } if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { t.Fatalf("Unsetenv failed: %v", err) } From 1910ae3ffb35ea6d4df5e77d813c64d2985b3ca5 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 14 Jan 2026 16:20:09 +0100 Subject: [PATCH 03/18] fix agent path for cf-metrics-exporter --- src/java/frameworks/cf_metrics_exporter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 98ffb74ec..13668bbbd 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -53,7 +53,7 @@ func (f *CfMetricsExporterFramework) Supply() error { if err != nil { return err } - agentDir := filepath.Join(f.ctx.Stager.DepDir(), ".java-buildpack", cfMetricsExporterDirName) + agentDir := filepath.Join(f.ctx.Stager.DepDir(), cfMetricsExporterDirName) if err := os.MkdirAll(agentDir, 0755); err != nil { return fmt.Errorf("failed to create agent dir: %w", err) } @@ -77,7 +77,7 @@ func (f *CfMetricsExporterFramework) Finalize() error { return err } jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) - agentPath := filepath.Join(".java-buildpack", cfMetricsExporterDirName, jarName) + agentPath := filepath.Join(f.ctx.Stager.DepDir(), cfMetricsExporterDirName, jarName) props := os.Getenv("CF_METRICS_EXPORTER_PROPS") var javaOpt string if props != "" { From 227d181954b7458dc3fdd9f190edcda23681cb6d Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 14 Jan 2026 16:49:34 +0100 Subject: [PATCH 04/18] fix agent path for cf-metrics-exporter using the proper deps dir and deps idx --- src/java/frameworks/cf_metrics_exporter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 13668bbbd..ddd057bf3 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -77,7 +77,8 @@ func (f *CfMetricsExporterFramework) Finalize() error { return err } jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) - agentPath := filepath.Join(f.ctx.Stager.DepDir(), cfMetricsExporterDirName, jarName) + depsIdx := f.ctx.Stager.DepsIdx() + agentPath := fmt.Sprintf("$DEPS_DIR/%s/cf_metrics_exporter/%s", depsIdx, jarName) props := os.Getenv("CF_METRICS_EXPORTER_PROPS") var javaOpt string if props != "" { From f9fe4b60d380258a6d7215f0f171975625e8650f Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 14 Jan 2026 20:52:39 +0100 Subject: [PATCH 05/18] add CF_METRICS_EXPORTER_ENABLED="test" for test purpose --- src/java/frameworks/cf_metrics_exporter.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index ddd057bf3..889df40cd 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -22,7 +22,7 @@ func NewCfMetricsExporterFramework(ctx *common.Context) *CfMetricsExporterFramew func (f *CfMetricsExporterFramework) Detect() (string, error) { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") - if enabled == "true" || enabled == "TRUE" { + if enabled == "true" || enabled == "TRUE" || enabled == "test" { version, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) @@ -69,7 +69,7 @@ func (f *CfMetricsExporterFramework) Supply() error { func (f *CfMetricsExporterFramework) Finalize() error { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") - if enabled != "true" && enabled != "TRUE" { + if enabled != "true" && enabled != "TRUE" && enabled != "test" { return nil } dep, _, err := f.getManifestDependency() @@ -86,6 +86,10 @@ func (f *CfMetricsExporterFramework) Finalize() error { } else { javaOpt = fmt.Sprintf("-javaagent:%s", agentPath) } + // Set noop JAVA_OPTS for testing purposes + if enabled == "test" { + javaOpt = "-XX:+PrintFlagsFinal" + } // Priority 43: after SkyWalking (41), Splunk OTEL (42) return writeJavaOptsFile(f.ctx, 43, cfMetricsExporterDirName, javaOpt) } From 81bc43855b666c5ba549853d32f288756f450b9c Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 14 Jan 2026 22:18:56 +0100 Subject: [PATCH 06/18] add CF_METRICS_EXPORTER_ENABLED="test" for test purpose: forgot one check --- src/java/frameworks/cf_metrics_exporter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 889df40cd..0aad9f0cd 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -46,7 +46,7 @@ func (f *CfMetricsExporterFramework) getManifestDependency() (libbuildpack.Depen func (f *CfMetricsExporterFramework) Supply() error { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") - if enabled != "true" && enabled != "TRUE" { + if enabled != "true" && enabled != "TRUE" && enabled != "test" { return nil } dep, _, err := f.getManifestDependency() From ac1c7789c1615408987bf4607e2503622796fe5c Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 15 Jan 2026 07:30:34 +0100 Subject: [PATCH 07/18] added fix and test for location of cf-metrics-exporter jar --- src/java/frameworks/cf_metrics_exporter.go | 37 ++++++++-- .../frameworks/cf_metrics_exporter_test.go | 73 +++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 0aad9f0cd..9dd8f1e69 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -12,12 +12,25 @@ import ( const cfMetricsExporterDependencyName = "cf-metrics-exporter" const cfMetricsExporterDirName = "cf_metrics_exporter" +// Installer interface for dependency installation +// Allows for mocking in tests +// Only the InstallDependency method is needed for this framework +// (matches the signature of libbuildpack.Installer) +type Installer interface { + InstallDependency(dep libbuildpack.Dependency, outputDir string) error +} + type CfMetricsExporterFramework struct { - ctx *common.Context + ctx *common.Context + installer Installer } func NewCfMetricsExporterFramework(ctx *common.Context) *CfMetricsExporterFramework { - return &CfMetricsExporterFramework{ctx: ctx} + installer := ctx.Installer + if installer == nil { + installer = libbuildpack.NewInstaller(ctx.Manifest) + } + return &CfMetricsExporterFramework{ctx: ctx, installer: installer} } func (f *CfMetricsExporterFramework) Detect() (string, error) { @@ -58,11 +71,25 @@ func (f *CfMetricsExporterFramework) Supply() error { return fmt.Errorf("failed to create agent dir: %w", err) } jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) - agentPath := filepath.Join(agentDir, jarName) - if _, err := os.Stat(agentPath); os.IsNotExist(err) { - if err := f.ctx.Installer.InstallDependency(dep, agentPath); err != nil { + jarPath := filepath.Join(agentDir, jarName) + if _, err := os.Stat(jarPath); os.IsNotExist(err) { + if err := f.installer.InstallDependency(dep, agentDir); err != nil { return fmt.Errorf("failed to download cf-metrics-exporter: %w", err) } + // Find the actual JAR file and rename if needed + files, err := os.ReadDir(agentDir) + if err != nil { + return fmt.Errorf("failed to read agent dir: %w", err) + } + for _, file := range files { + if filepath.Ext(file.Name()) == ".jar" && file.Name() != jarName { + src := filepath.Join(agentDir, file.Name()) + if err := os.Rename(src, jarPath); err != nil { + return fmt.Errorf("failed to rename jar: %w", err) + } + break + } + } } return nil } diff --git a/src/java/frameworks/cf_metrics_exporter_test.go b/src/java/frameworks/cf_metrics_exporter_test.go index 6e62720a9..ac549dd32 100644 --- a/src/java/frameworks/cf_metrics_exporter_test.go +++ b/src/java/frameworks/cf_metrics_exporter_test.go @@ -64,3 +64,76 @@ func TestDetectDisabledWithRealManifest(t *testing.T) { t.Fatalf("Unsetenv failed: %v", err) } } + +func TestSupplyPlacesJarCorrectly(t *testing.T) { + if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "true"); err != nil { + t.Fatalf("Setenv failed: %v", err) + } + if err := os.Setenv("CF_STACK", "cflinuxfs4"); err != nil { + t.Fatalf("Setenv failed: %v", err) + } + manifestDir := filepath.Join("../../../") + logger := libbuildpack.NewLogger(os.Stdout) + manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) + if err != nil { + t.Fatalf("Failed to load manifest.yml: %v", err) + } + tmpDepDir, err := os.MkdirTemp("", "cf_metrics_exporter_test") + if err != nil { + t.Fatalf("Failed to create temp dep dir: %v", err) + } + defer os.RemoveAll(tmpDepDir) + args := []string{"", "", tmpDepDir, "0"} + ctx := &common.Context{Manifest: manifest} + ctx.Stager = libbuildpack.NewStager(args, logger, manifest) + // Do not set ctx.Installer, pass mock to framework constructor + f := &CfMetricsExporterFramework{ctx: ctx, installer: &mockInstallerJar{}} + if err := f.Supply(); err != nil { + t.Fatalf("Supply() error: %v", err) + } + // Check the JAR file exists directly in cf_metrics_exporter + jarName := "cf-metrics-exporter-0.7.1.jar" // adjust if version changes in manifest + jarPath := filepath.Join(tmpDepDir, "cf_metrics_exporter", jarName) + // Print directory contents for debugging + dirPath := filepath.Join(tmpDepDir, "cf_metrics_exporter") + dirEntries, dirErr := os.ReadDir(dirPath) + if dirErr != nil { + t.Errorf("Error reading cf_metrics_exporter dir: %v", dirErr) + } else { + for _, entry := range dirEntries { + t.Logf("Found in cf_metrics_exporter: %s", entry.Name()) + } + } + if fi, err := os.Stat(jarPath); err != nil { + t.Errorf("JAR file not found at expected path: %s, error: %v", jarPath, err) + } else if fi.IsDir() { + t.Errorf("Expected file but found directory at: %s", jarPath) + } + // Check there is NOT a directory named after the JAR inside cf_metrics_exporter + badDir := filepath.Join(tmpDepDir, "cf_metrics_exporter", jarName) + if fi, err := os.Stat(badDir); err == nil && fi.IsDir() { + t.Errorf("Unexpected directory found: %s", badDir) + } + if err := os.Unsetenv("CF_STACK"); err != nil { + t.Fatalf("Unsetenv failed: %v", err) + } + if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { + t.Fatalf("Unsetenv failed: %v", err) + } +} + +type mockInstallerJar struct{} + +func (m *mockInstallerJar) InstallDependency(dep libbuildpack.Dependency, outputDir string) error { + // Simulate a successful download: create the agent dir and a JAR file with the expected name + if err := os.MkdirAll(outputDir, 0755); err != nil { + return err + } + jarName := "cf-metrics-exporter-0.7.1.jar" + jarPath := filepath.Join(outputDir, jarName) + f, err := os.Create(jarPath) + if err != nil { + return err + } + return f.Close() +} From 9fc76700fbb973a36c84223e03bab20f4d828c9a Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 15 Jan 2026 07:56:13 +0100 Subject: [PATCH 08/18] fix cf-metrics-exporter framework name returned from Detect --- src/java/frameworks/cf_metrics_exporter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 9dd8f1e69..ff0ad8b80 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -40,7 +40,7 @@ func (f *CfMetricsExporterFramework) Detect() (string, error) { if err != nil { return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } - return fmt.Sprintf("cf-metrics-exporter=%s", version), nil + return fmt.Sprintf("cf-metrics-exporter-%s", version), nil } return "", nil } From 382deed836810c00ad7484e4993c01aff51d2a94 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 15 Jan 2026 08:53:12 +0100 Subject: [PATCH 09/18] add commas to frameworks log, update JMX framework name --- src/java/finalize/finalize.go | 6 ++++-- src/java/frameworks/cf_metrics_exporter.go | 2 +- src/java/frameworks/jmx.go | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/java/finalize/finalize.go b/src/java/finalize/finalize.go index 00117cae4..551bb8e04 100644 --- a/src/java/finalize/finalize.go +++ b/src/java/finalize/finalize.go @@ -2,9 +2,11 @@ package finalize import ( "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" "os" "path/filepath" + "strings" + + "github.com/cloudfoundry/java-buildpack/src/java/common" "github.com/cloudfoundry/java-buildpack/src/java/containers" "github.com/cloudfoundry/java-buildpack/src/java/frameworks" @@ -157,7 +159,7 @@ func (f *Finalizer) finalizeFrameworks() error { return nil } - f.Log.Info("Finalizing frameworks: %v", frameworkNames) + f.Log.Info("Finalizing frameworks: %v", strings.Join(frameworkNames, ",")) // Finalize all detected frameworks for i, framework := range detectedFrameworks { diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index ff0ad8b80..6b0b476f0 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -40,7 +40,7 @@ func (f *CfMetricsExporterFramework) Detect() (string, error) { if err != nil { return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } - return fmt.Sprintf("cf-metrics-exporter-%s", version), nil + return fmt.Sprintf("%s (%s)", version.Name, version.Version), nil } return "", nil } diff --git a/src/java/frameworks/jmx.go b/src/java/frameworks/jmx.go index 96e29b630..f34f03b71 100644 --- a/src/java/frameworks/jmx.go +++ b/src/java/frameworks/jmx.go @@ -2,9 +2,10 @@ package frameworks import ( "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" "os" "strconv" + + "github.com/cloudfoundry/java-buildpack/src/java/common" ) // JmxFramework implements JMX (Java Management Extensions) support @@ -27,7 +28,7 @@ func (j *JmxFramework) Detect() (string, error) { } port := j.getPort() - return fmt.Sprintf("jmx=%d", port), nil + return fmt.Sprintf("JMX (port %d)", port), nil } // Supply performs JMX setup during supply phase From 7b96ebc3bb1e520fcdb5d9236ff3bc258a7f7c9f Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 16 Jan 2026 09:56:03 +0100 Subject: [PATCH 10/18] remove temp test construct for cf-metrics-exporter --- src/java/frameworks/cf_metrics_exporter.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 6b0b476f0..a842c8d3f 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -35,7 +35,7 @@ func NewCfMetricsExporterFramework(ctx *common.Context) *CfMetricsExporterFramew func (f *CfMetricsExporterFramework) Detect() (string, error) { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") - if enabled == "true" || enabled == "TRUE" || enabled == "test" { + if enabled == "true" || enabled == "TRUE" { version, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) @@ -59,7 +59,7 @@ func (f *CfMetricsExporterFramework) getManifestDependency() (libbuildpack.Depen func (f *CfMetricsExporterFramework) Supply() error { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") - if enabled != "true" && enabled != "TRUE" && enabled != "test" { + if enabled != "true" && enabled != "TRUE" { return nil } dep, _, err := f.getManifestDependency() @@ -96,7 +96,7 @@ func (f *CfMetricsExporterFramework) Supply() error { func (f *CfMetricsExporterFramework) Finalize() error { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") - if enabled != "true" && enabled != "TRUE" && enabled != "test" { + if enabled != "true" && enabled != "TRUE" { return nil } dep, _, err := f.getManifestDependency() @@ -113,10 +113,6 @@ func (f *CfMetricsExporterFramework) Finalize() error { } else { javaOpt = fmt.Sprintf("-javaagent:%s", agentPath) } - // Set noop JAVA_OPTS for testing purposes - if enabled == "test" { - javaOpt = "-XX:+PrintFlagsFinal" - } // Priority 43: after SkyWalking (41), Splunk OTEL (42) return writeJavaOptsFile(f.ctx, 43, cfMetricsExporterDirName, javaOpt) } From 36aa0bbb0bf25eef6dd70fe40c5047519329b7e0 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 19 Jan 2026 10:24:13 +0100 Subject: [PATCH 11/18] update cf-metrics-exporter docs --- docs/framework-cf_metrics_exporter.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/framework-cf_metrics_exporter.md b/docs/framework-cf_metrics_exporter.md index e731ac223..3d2d44b46 100644 --- a/docs/framework-cf_metrics_exporter.md +++ b/docs/framework-cf_metrics_exporter.md @@ -4,7 +4,7 @@ This framework integrates the [cf-metrics-exporter](https://github.com/rabobank/ ## Enabling the Exporter -Set the following environment variable to enable the agent: +Set the following environment variable in the cloud foundry env to enable the agent (via manifest.yml or `cf set-env`): ``` CF_METRICS_EXPORTER_ENABLED=true @@ -13,7 +13,7 @@ CF_METRICS_EXPORTER_ENABLED=true ## Configuration - **CF_METRICS_EXPORTER_ENABLED**: Set to `true` to enable the agent (default: disabled). -- **CF_METRICS_EXPORTER_PROPS**: (Optional) Properties string to pass to the agent, e.g. `port=9090,foo=bar`. +- **CF_METRICS_EXPORTER_PROPS**: (Optional) Properties string to pass to the agent, e.g. `enableLogEmitter,rpsType=tomcat-bean`. ## How it Works @@ -25,7 +25,7 @@ CF_METRICS_EXPORTER_ENABLED=true ``` CF_METRICS_EXPORTER_ENABLED=true -CF_METRICS_EXPORTER_PROPS="port=9090,foo=bar" +CF_METRICS_EXPORTER_PROPS="enableLogEmitter,rpsType=tomcat-bean" ``` ## Version @@ -36,5 +36,5 @@ CF_METRICS_EXPORTER_PROPS="port=9090,foo=bar" ## Notes - The agent is injected with priority 43 in JAVA_OPTS (after other APM agents). -- The agent JAR is placed in `.java-buildpack/cf_metrics_exporter/` within the dependency directory. + From aed499d6775ebfc547872454e0085e0e5246a1f2 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 19 Jan 2026 12:47:55 +0100 Subject: [PATCH 12/18] improve cf-metrics-exporter unit test --- .../frameworks/cf_metrics_exporter_test.go | 146 ++++++++---------- 1 file changed, 66 insertions(+), 80 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter_test.go b/src/java/frameworks/cf_metrics_exporter_test.go index ac549dd32..86c494388 100644 --- a/src/java/frameworks/cf_metrics_exporter_test.go +++ b/src/java/frameworks/cf_metrics_exporter_test.go @@ -10,22 +10,44 @@ import ( "github.com/cloudfoundry/libbuildpack" ) -func TestDetectEnabledWithRealManifest(t *testing.T) { - if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "true"); err != nil { - t.Fatalf("Setenv failed: %v", err) +// Helper functions for test setup +func setEnvVars(t *testing.T, vars map[string]string) { + for k, v := range vars { + if err := os.Setenv(k, v); err != nil { + t.Fatalf("Setenv %s failed: %v", k, err) + } } - // needed to match the cf-metrics-exporter dependency in the manifest - if err := os.Setenv("CF_STACK", "cflinuxfs4"); err != nil { - t.Fatalf("Setenv failed: %v", err) +} + +func unsetEnvVars(t *testing.T, vars []string) { + for _, k := range vars { + if err := os.Unsetenv(k); err != nil { + t.Fatalf("Unsetenv %s failed: %v", k, err) + } } +} + +func loadManifest(t *testing.T) *libbuildpack.Manifest { manifestDir := filepath.Join("../../../") logger := libbuildpack.NewLogger(os.Stdout) manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) if err != nil { t.Fatalf("Failed to load manifest.yml: %v", err) } + return manifest +} + +func TestDetectEnabledWithRealManifest(t *testing.T) { + setEnvVars(t, map[string]string{ + "CF_METRICS_EXPORTER_ENABLED": "true", + "CF_STACK": "cflinuxfs4", + }) + defer unsetEnvVars(t, []string{"CF_METRICS_EXPORTER_ENABLED", "CF_STACK"}) + + manifest := loadManifest(t) ctx := &common.Context{Manifest: manifest} f := NewCfMetricsExporterFramework(ctx) + name, err := f.Detect() if err != nil { t.Fatalf("Detect() error: %v", err) @@ -33,26 +55,16 @@ func TestDetectEnabledWithRealManifest(t *testing.T) { if name == "" { t.Error("Detect() should return non-empty name when enabled") } - if err := os.Unsetenv("CF_STACK"); err != nil { - t.Fatalf("Unsetenv failed: %v", err) - } - if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { - t.Fatalf("Unsetenv failed: %v", err) - } } func TestDetectDisabledWithRealManifest(t *testing.T) { - if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "false"); err != nil { - t.Fatalf("Setenv failed: %v", err) - } - manifestDir := filepath.Join("../../../") - logger := libbuildpack.NewLogger(os.Stdout) - manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) - if err != nil { - t.Fatalf("Failed to load manifest.yml: %v", err) - } + setEnvVars(t, map[string]string{"CF_METRICS_EXPORTER_ENABLED": "false"}) + defer unsetEnvVars(t, []string{"CF_METRICS_EXPORTER_ENABLED"}) + + manifest := loadManifest(t) ctx := &common.Context{Manifest: manifest} f := NewCfMetricsExporterFramework(ctx) + name, err := f.Detect() if err != nil { t.Fatalf("Detect() error: %v", err) @@ -60,80 +72,54 @@ func TestDetectDisabledWithRealManifest(t *testing.T) { if name != "" { t.Error("Detect() should return empty name when disabled") } - if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { - t.Fatalf("Unsetenv failed: %v", err) - } } func TestSupplyPlacesJarCorrectly(t *testing.T) { - if err := os.Setenv("CF_METRICS_EXPORTER_ENABLED", "true"); err != nil { - t.Fatalf("Setenv failed: %v", err) - } - if err := os.Setenv("CF_STACK", "cflinuxfs4"); err != nil { - t.Fatalf("Setenv failed: %v", err) - } - manifestDir := filepath.Join("../../../") - logger := libbuildpack.NewLogger(os.Stdout) - manifest, err := libbuildpack.NewManifest(manifestDir, logger, time.Now()) - if err != nil { - t.Fatalf("Failed to load manifest.yml: %v", err) - } + setEnvVars(t, map[string]string{ + "CF_METRICS_EXPORTER_ENABLED": "true", + "CF_STACK": "cflinuxfs4", + }) + defer unsetEnvVars(t, []string{"CF_METRICS_EXPORTER_ENABLED", "CF_STACK"}) + + manifest := loadManifest(t) + // Setup temp dependency dir tmpDepDir, err := os.MkdirTemp("", "cf_metrics_exporter_test") if err != nil { t.Fatalf("Failed to create temp dep dir: %v", err) } - defer os.RemoveAll(tmpDepDir) + defer func() { + _ = os.RemoveAll(tmpDepDir) + }() + args := []string{"", "", tmpDepDir, "0"} ctx := &common.Context{Manifest: manifest} - ctx.Stager = libbuildpack.NewStager(args, logger, manifest) - // Do not set ctx.Installer, pass mock to framework constructor - f := &CfMetricsExporterFramework{ctx: ctx, installer: &mockInstallerJar{}} - if err := f.Supply(); err != nil { - t.Fatalf("Supply() error: %v", err) - } - // Check the JAR file exists directly in cf_metrics_exporter + ctx.Stager = libbuildpack.NewStager(args, libbuildpack.NewLogger(os.Stdout), manifest) + + // Pre-create the expected JAR file jarName := "cf-metrics-exporter-0.7.1.jar" // adjust if version changes in manifest - jarPath := filepath.Join(tmpDepDir, "cf_metrics_exporter", jarName) - // Print directory contents for debugging - dirPath := filepath.Join(tmpDepDir, "cf_metrics_exporter") - dirEntries, dirErr := os.ReadDir(dirPath) - if dirErr != nil { - t.Errorf("Error reading cf_metrics_exporter dir: %v", dirErr) - } else { - for _, entry := range dirEntries { - t.Logf("Found in cf_metrics_exporter: %s", entry.Name()) - } - } - if fi, err := os.Stat(jarPath); err != nil { - t.Errorf("JAR file not found at expected path: %s, error: %v", jarPath, err) - } else if fi.IsDir() { - t.Errorf("Expected file but found directory at: %s", jarPath) + jarDir := filepath.Join(tmpDepDir, "cf_metrics_exporter") + if err := os.MkdirAll(jarDir, 0755); err != nil { + t.Fatalf("Failed to create jar dir: %v", err) } - // Check there is NOT a directory named after the JAR inside cf_metrics_exporter - badDir := filepath.Join(tmpDepDir, "cf_metrics_exporter", jarName) - if fi, err := os.Stat(badDir); err == nil && fi.IsDir() { - t.Errorf("Unexpected directory found: %s", badDir) - } - if err := os.Unsetenv("CF_STACK"); err != nil { - t.Fatalf("Unsetenv failed: %v", err) + jarPath := filepath.Join(jarDir, jarName) + fJar, err := os.Create(jarPath) + if err != nil { + t.Fatalf("Failed to create jar file: %v", err) } - if err := os.Unsetenv("CF_METRICS_EXPORTER_ENABLED"); err != nil { - t.Fatalf("Unsetenv failed: %v", err) + if err := fJar.Close(); err != nil { + t.Fatalf("Failed to close jar file: %v", err) } -} -type mockInstallerJar struct{} + f := NewCfMetricsExporterFramework(ctx) -func (m *mockInstallerJar) InstallDependency(dep libbuildpack.Dependency, outputDir string) error { - // Simulate a successful download: create the agent dir and a JAR file with the expected name - if err := os.MkdirAll(outputDir, 0755); err != nil { - return err + if err := f.Supply(); err != nil { + t.Fatalf("Supply() error: %v", err) } - jarName := "cf-metrics-exporter-0.7.1.jar" - jarPath := filepath.Join(outputDir, jarName) - f, err := os.Create(jarPath) - if err != nil { - return err + + // Assert JAR file exists directly in cf_metrics_exporter + if fi, err := os.Stat(jarPath); err != nil { + t.Errorf("JAR file not found at expected path: %s, error: %v", jarPath, err) + } else if fi.IsDir() { + t.Errorf("Expected file but found directory at: %s", jarPath) } - return f.Close() } From 480957ed5375c6de450836f2482fcd79da23204a Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 19 Jan 2026 12:51:08 +0100 Subject: [PATCH 13/18] fix framework names of cf-metrics-exporter and JMX --- src/java/frameworks/cf_metrics_exporter.go | 4 ++-- src/java/frameworks/jmx.go | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index a842c8d3f..566d048b8 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -36,11 +36,11 @@ func NewCfMetricsExporterFramework(ctx *common.Context) *CfMetricsExporterFramew func (f *CfMetricsExporterFramework) Detect() (string, error) { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") if enabled == "true" || enabled == "TRUE" { - version, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) + _, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } - return fmt.Sprintf("%s (%s)", version.Name, version.Version), nil + return "CF Metrics Exporter", nil } return "", nil } diff --git a/src/java/frameworks/jmx.go b/src/java/frameworks/jmx.go index f34f03b71..791e69ed2 100644 --- a/src/java/frameworks/jmx.go +++ b/src/java/frameworks/jmx.go @@ -27,8 +27,7 @@ func (j *JmxFramework) Detect() (string, error) { return "", nil } - port := j.getPort() - return fmt.Sprintf("JMX (port %d)", port), nil + return "JMX", nil } // Supply performs JMX setup during supply phase From 4566bf59d8133689ffcdaea21fc6a4de9971553c Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 19 Jan 2026 16:22:49 +0100 Subject: [PATCH 14/18] cf-metrics-exporter: add config logging during supply phase --- src/java/frameworks/cf_metrics_exporter.go | 26 +++++--- .../frameworks/cf_metrics_exporter_test.go | 65 +++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 566d048b8..693745c50 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -21,7 +21,7 @@ type Installer interface { } type CfMetricsExporterFramework struct { - ctx *common.Context + context *common.Context installer Installer } @@ -30,13 +30,13 @@ func NewCfMetricsExporterFramework(ctx *common.Context) *CfMetricsExporterFramew if installer == nil { installer = libbuildpack.NewInstaller(ctx.Manifest) } - return &CfMetricsExporterFramework{ctx: ctx, installer: installer} + return &CfMetricsExporterFramework{context: ctx, installer: installer} } func (f *CfMetricsExporterFramework) Detect() (string, error) { enabled := os.Getenv("CF_METRICS_EXPORTER_ENABLED") if enabled == "true" || enabled == "TRUE" { - _, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) + _, err := f.context.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { return "", fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } @@ -46,11 +46,11 @@ func (f *CfMetricsExporterFramework) Detect() (string, error) { } func (f *CfMetricsExporterFramework) getManifestDependency() (libbuildpack.Dependency, *libbuildpack.ManifestEntry, error) { - dep, err := f.ctx.Manifest.DefaultVersion(cfMetricsExporterDependencyName) + dep, err := f.context.Manifest.DefaultVersion(cfMetricsExporterDependencyName) if err != nil { return libbuildpack.Dependency{}, nil, fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) } - entry, err := f.ctx.Manifest.GetEntry(dep) + entry, err := f.context.Manifest.GetEntry(dep) if err != nil { return dep, nil, fmt.Errorf("cf-metrics-exporter manifest entry not found: %w", err) } @@ -66,7 +66,7 @@ func (f *CfMetricsExporterFramework) Supply() error { if err != nil { return err } - agentDir := filepath.Join(f.ctx.Stager.DepDir(), cfMetricsExporterDirName) + agentDir := filepath.Join(f.context.Stager.DepDir(), cfMetricsExporterDirName) if err := os.MkdirAll(agentDir, 0755); err != nil { return fmt.Errorf("failed to create agent dir: %w", err) } @@ -91,6 +91,16 @@ func (f *CfMetricsExporterFramework) Supply() error { } } } + version, err := f.context.Manifest.DefaultVersion(cfMetricsExporterDependencyName) + if err != nil { + return fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) + } + props := os.Getenv("CF_METRICS_EXPORTER_PROPS") + if props != "" { + f.context.Log.BeginStep("CF Metrics Exporter v%s enabled, with properties: %s", version.Version, props) + } else { + f.context.Log.BeginStep("CF Metrics Exporter v%s enabled", version.Version) + } return nil } @@ -104,7 +114,7 @@ func (f *CfMetricsExporterFramework) Finalize() error { return err } jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) - depsIdx := f.ctx.Stager.DepsIdx() + depsIdx := f.context.Stager.DepsIdx() agentPath := fmt.Sprintf("$DEPS_DIR/%s/cf_metrics_exporter/%s", depsIdx, jarName) props := os.Getenv("CF_METRICS_EXPORTER_PROPS") var javaOpt string @@ -114,5 +124,5 @@ func (f *CfMetricsExporterFramework) Finalize() error { javaOpt = fmt.Sprintf("-javaagent:%s", agentPath) } // Priority 43: after SkyWalking (41), Splunk OTEL (42) - return writeJavaOptsFile(f.ctx, 43, cfMetricsExporterDirName, javaOpt) + return writeJavaOptsFile(f.context, 43, cfMetricsExporterDirName, javaOpt) } diff --git a/src/java/frameworks/cf_metrics_exporter_test.go b/src/java/frameworks/cf_metrics_exporter_test.go index 86c494388..a1057aff3 100644 --- a/src/java/frameworks/cf_metrics_exporter_test.go +++ b/src/java/frameworks/cf_metrics_exporter_test.go @@ -3,6 +3,7 @@ package frameworks import ( "os" "path/filepath" + "strings" "testing" "time" @@ -46,6 +47,7 @@ func TestDetectEnabledWithRealManifest(t *testing.T) { manifest := loadManifest(t) ctx := &common.Context{Manifest: manifest} + ctx.Log = libbuildpack.NewLogger(os.Stdout) f := NewCfMetricsExporterFramework(ctx) name, err := f.Detect() @@ -63,6 +65,7 @@ func TestDetectDisabledWithRealManifest(t *testing.T) { manifest := loadManifest(t) ctx := &common.Context{Manifest: manifest} + ctx.Log = libbuildpack.NewLogger(os.Stdout) f := NewCfMetricsExporterFramework(ctx) name, err := f.Detect() @@ -94,6 +97,7 @@ func TestSupplyPlacesJarCorrectly(t *testing.T) { args := []string{"", "", tmpDepDir, "0"} ctx := &common.Context{Manifest: manifest} ctx.Stager = libbuildpack.NewStager(args, libbuildpack.NewLogger(os.Stdout), manifest) + ctx.Log = libbuildpack.NewLogger(os.Stdout) // Pre-create the expected JAR file jarName := "cf-metrics-exporter-0.7.1.jar" // adjust if version changes in manifest @@ -123,3 +127,64 @@ func TestSupplyPlacesJarCorrectly(t *testing.T) { t.Errorf("Expected file but found directory at: %s", jarPath) } } + +func TestSupplyLogsProps(t *testing.T) { + setEnvVars(t, map[string]string{ + "CF_METRICS_EXPORTER_ENABLED": "true", + "CF_STACK": "cflinuxfs4", + "CF_METRICS_EXPORTER_PROPS": "foo=bar,abc=123", + }) + defer unsetEnvVars(t, []string{"CF_METRICS_EXPORTER_ENABLED", "CF_STACK", "CF_METRICS_EXPORTER_PROPS"}) + + manifest := loadManifest(t) + tmpDepDir, err := os.MkdirTemp("", "cf_metrics_exporter_test_props") + if err != nil { + t.Fatalf("Failed to create temp dep dir: %v", err) + } + defer func() { _ = os.RemoveAll(tmpDepDir) }() + + args := []string{"", "", tmpDepDir, "0"} + ctx := &common.Context{Manifest: manifest} + ctx.Stager = libbuildpack.NewStager(args, libbuildpack.NewLogger(os.Stdout), manifest) + + // Pre-create the expected JAR file + jarName := "cf-metrics-exporter-0.7.1.jar" + jarDir := filepath.Join(tmpDepDir, "cf_metrics_exporter") + if err := os.MkdirAll(jarDir, 0755); err != nil { + t.Fatalf("Failed to create jar dir: %v", err) + } + jarPath := filepath.Join(jarDir, jarName) + fJar, err := os.Create(jarPath) + if err != nil { + t.Fatalf("Failed to create jar file: %v", err) + } + if err := fJar.Close(); err != nil { + t.Fatalf("Failed to close jar file: %v", err) + } + + // Capture log output + logBuf := &logBuffer{} + ctx.Log = libbuildpack.NewLogger(logBuf) + + f := NewCfMetricsExporterFramework(ctx) + if err := f.Supply(); err != nil { + t.Fatalf("Supply() error: %v", err) + } + + if got := logBuf.String(); !strings.Contains(got, "foo=bar,abc=123") { + t.Errorf("Expected log to contain CF_METRICS_EXPORTER_PROPS value, got: %s", got) + } +} + +type logBuffer struct { + buf []byte +} + +func (l *logBuffer) Write(p []byte) (n int, err error) { + l.buf = append(l.buf, p...) + return len(p), nil +} + +func (l *logBuffer) String() string { + return string(l.buf) +} From b93dedbf163d66771ce9cc4b513bfe79986bc7c9 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 20 Jan 2026 08:07:16 +0100 Subject: [PATCH 15/18] PR comment fix: revert JMX changes --- src/java/frameworks/jmx.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/java/frameworks/jmx.go b/src/java/frameworks/jmx.go index 791e69ed2..96e29b630 100644 --- a/src/java/frameworks/jmx.go +++ b/src/java/frameworks/jmx.go @@ -2,10 +2,9 @@ package frameworks import ( "fmt" + "github.com/cloudfoundry/java-buildpack/src/java/common" "os" "strconv" - - "github.com/cloudfoundry/java-buildpack/src/java/common" ) // JmxFramework implements JMX (Java Management Extensions) support @@ -27,7 +26,8 @@ func (j *JmxFramework) Detect() (string, error) { return "", nil } - return "JMX", nil + port := j.getPort() + return fmt.Sprintf("jmx=%d", port), nil } // Supply performs JMX setup during supply phase From 9dc0a9c3e4b260a8aa095b18edb063458a73e631 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 20 Jan 2026 13:43:50 +0100 Subject: [PATCH 16/18] PR comment fix: download from maven central instead of github --- manifest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.yml b/manifest.yml index 952e6cc70..1aecc2d5c 100644 --- a/manifest.yml +++ b/manifest.yml @@ -549,7 +549,7 @@ dependencies: # cf-metrics-exporter Agent - name: cf-metrics-exporter version: 0.7.1 - uri: https://github.com/rabobank/cf-metrics-exporter/releases/download/0.7.1/cf-metrics-exporter-0.7.1.jar + uri: https://repo1.maven.org/maven2/io/github/rabobank/cf-metrics-exporter/0.7.1/cf-metrics-exporter-0.7.1.jar sha256: 7ebabd3ffd812082cf92a513c8d2ac52906f5b42cd952cbe740bd5d5b086e79b cf_stacks: - cflinuxfs4 From 5c8d7f77f8298632cc0dc651207ff3e52a38981b Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 20 Jan 2026 13:49:05 +0100 Subject: [PATCH 17/18] PR comment fix: simplify cf-metrics-exporter jar download and install --- src/java/frameworks/cf_metrics_exporter.go | 36 +++++++++------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 693745c50..4bf85990e 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -62,45 +62,39 @@ func (f *CfMetricsExporterFramework) Supply() error { if enabled != "true" && enabled != "TRUE" { return nil } + dep, _, err := f.getManifestDependency() if err != nil { return err } + agentDir := filepath.Join(f.context.Stager.DepDir(), cfMetricsExporterDirName) + jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) + jarPath := filepath.Join(agentDir, jarName) + + // Ensure agent directory exists if err := os.MkdirAll(agentDir, 0755); err != nil { return fmt.Errorf("failed to create agent dir: %w", err) } - jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) - jarPath := filepath.Join(agentDir, jarName) + + // Download the JAR if not present if _, err := os.Stat(jarPath); os.IsNotExist(err) { if err := f.installer.InstallDependency(dep, agentDir); err != nil { return fmt.Errorf("failed to download cf-metrics-exporter: %w", err) } - // Find the actual JAR file and rename if needed - files, err := os.ReadDir(agentDir) - if err != nil { - return fmt.Errorf("failed to read agent dir: %w", err) - } - for _, file := range files { - if filepath.Ext(file.Name()) == ".jar" && file.Name() != jarName { - src := filepath.Join(agentDir, file.Name()) - if err := os.Rename(src, jarPath); err != nil { - return fmt.Errorf("failed to rename jar: %w", err) - } - break - } + if _, err := os.Stat(jarPath); err != nil { + return fmt.Errorf("expected jar file not found after download: %w", err) } } - version, err := f.context.Manifest.DefaultVersion(cfMetricsExporterDependencyName) - if err != nil { - return fmt.Errorf("cf-metrics-exporter version not found in manifest: %w", err) - } + + // Log activation, including properties if set props := os.Getenv("CF_METRICS_EXPORTER_PROPS") if props != "" { - f.context.Log.BeginStep("CF Metrics Exporter v%s enabled, with properties: %s", version.Version, props) + f.context.Log.Info("CF Metrics Exporter v%s enabled, with properties: %s", dep.Version, props) } else { - f.context.Log.BeginStep("CF Metrics Exporter v%s enabled", version.Version) + f.context.Log.Info("CF Metrics Exporter v%s enabled", dep.Version) } + return nil } From 68818b4d15afb6c1e0c4a1983ee84ebc2db4201f Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Tue, 20 Jan 2026 15:46:44 +0100 Subject: [PATCH 18/18] added spacing for readability --- src/java/frameworks/cf_metrics_exporter.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/java/frameworks/cf_metrics_exporter.go b/src/java/frameworks/cf_metrics_exporter.go index 4bf85990e..7222727d3 100644 --- a/src/java/frameworks/cf_metrics_exporter.go +++ b/src/java/frameworks/cf_metrics_exporter.go @@ -103,13 +103,16 @@ func (f *CfMetricsExporterFramework) Finalize() error { if enabled != "true" && enabled != "TRUE" { return nil } + dep, _, err := f.getManifestDependency() if err != nil { return err } + jarName := fmt.Sprintf("cf-metrics-exporter-%s.jar", dep.Version) depsIdx := f.context.Stager.DepsIdx() agentPath := fmt.Sprintf("$DEPS_DIR/%s/cf_metrics_exporter/%s", depsIdx, jarName) + props := os.Getenv("CF_METRICS_EXPORTER_PROPS") var javaOpt string if props != "" { @@ -117,6 +120,7 @@ func (f *CfMetricsExporterFramework) Finalize() error { } else { javaOpt = fmt.Sprintf("-javaagent:%s", agentPath) } + // Priority 43: after SkyWalking (41), Splunk OTEL (42) return writeJavaOptsFile(f.context, 43, cfMetricsExporterDirName, javaOpt) }