diff --git a/README.md b/README.md
index c2da21279d..6f1e07d0b6 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,7 @@ The buildpack supports extension through the use of Git repository forking. The
* [Debug](docs/framework-debug.md) ([Configuration](docs/framework-debug.md#configuration))
* [Dyadic EKM Security Provider](docs/framework-dyadic_ekm_security_provider.md) ([Configuration](docs/framework-dyadic_ekm_security_provider.md#configuration))
* [Dynatrace Appmon Agent](docs/framework-dynatrace_appmon_agent.md) ([Configuration](docs/framework-dynatrace_appmon_agent.md#configuration))
+ * [Elastic APM Agent](docs/framework-elastic_apm_agent.md) ([Configuration](docs/framework-elastic_apm_agent.md#configuration))
* [Dynatrace SaaS/Managed OneAgent](docs/framework-dynatrace_one_agent.md) ([Configuration](docs/framework-dynatrace_one_agent.md#configuration))
* [Google Stackdriver Debugger](docs/framework-google_stackdriver_debugger.md) ([Configuration](docs/framework-google_stackdriver_debugger.md#configuration))
* [Google Stackdriver Profiler](docs/framework-google_stackdriver_profiler.md) ([Configuration](docs/framework-google_stackdriver_profiler.md#configuration))
diff --git a/config/components.yml b/config/components.yml
index 1863871b04..4cc4ea7ca5 100644
--- a/config/components.yml
+++ b/config/components.yml
@@ -51,6 +51,7 @@ frameworks:
- "JavaBuildpack::Framework::DyadicEkmSecurityProvider"
- "JavaBuildpack::Framework::DynatraceAppmonAgent"
- "JavaBuildpack::Framework::DynatraceOneAgent"
+ - "JavaBuildpack::Framework::ElasticApmAgent"
- "JavaBuildpack::Framework::GoogleStackdriverDebugger"
- "JavaBuildpack::Framework::GoogleStackdriverProfiler"
- "JavaBuildpack::Framework::IntroscopeAgent"
diff --git a/config/elastic_apm_agent.yml b/config/elastic_apm_agent.yml
new file mode 100644
index 0000000000..c41ace3ffa
--- /dev/null
+++ b/config/elastic_apm_agent.yml
@@ -0,0 +1,19 @@
+# Cloud Foundry Java Buildpack
+# Copyright 2013-2018 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.
+
+# Configuration for the Elastic Apm Agent framework
+---
+version: 1.+
+repository_root: https://raw.githubusercontent.com/elastic/apm-agent-java/master/cloudfoundry
diff --git a/docs/framework-elastic_apm_agent.md b/docs/framework-elastic_apm_agent.md
new file mode 100644
index 0000000000..c2975c8682
--- /dev/null
+++ b/docs/framework-elastic_apm_agent.md
@@ -0,0 +1,69 @@
+# Elastic APM Agent Framework
+
+The Elastic APM Agent Framework causes an application to be automatically configured to work with [Elastic APM][].
+
+
+
+ Detection Criterion |
+ Existence of a single bound Elastic APM service. The existence of an Elastic APM service defined by the VCAP_SERVICES payload containing a service name, label or tag with elasticapm or elastic-apm as a substring.
+ |
+
+ Tags |
+ elastic-apm-agent=<version> |
+
+
+Tags are printed to standard output by the buildpack detect script
+
+For more information regarding setup and configuration, please refer to the [Elastic APM with Pivotal Cloud Foundry tutorial][pivotal].
+
+## User-Provided Service
+When binding Elastic APM using a user-provided service, it must have name or tag with `elasticapm` or `elastic-apm` in it. The credential payload can contain the following entries.
+
+| Name | Description
+| ---- | -----------
+| `server_urls` | The URLs for the Elastic APM Server. They must be fully qualified, including protocol (http or https) and port.
+| `secret_token` (Optional)| This string is used to ensure that only your agents can send data to your APM server. Both the agents and the APM server have to be configured with the same secret token. Use if APM Server requires a token.
+| `***` (Optional) | Any additional entries will be applied as a system property appended to `-Delastic.apm.` to allow full configuration of the agent. See [Configuration of Elastic Agent][].
+
+
+### Creating an Elastic APM USer Provided Service
+Users must provide their own Elastic APM service. A user-provided Elastic APM service must have a name or tag with `elastic-apm` or `elasticapm` in it so that the Elastic APM Agent Framework will automatically configure the application to work with the service.
+
+Example of a minimal configuration:
+
+```
+cf cups my-elastic-apm-service -p '{"server_urls":"https://my-apm-server:8200","secret_token":"my-secret-token"}'
+```
+
+Example of a configuration with additional configuration parameters:
+
+```
+cf cups my-elastic-apm-service -p '{"server_urls":"https://my-apm-server:8200","secret_token":"","server_timeout":"10s","environment":"production"}'
+```
+
+Bind your application to the service using:
+
+`cf bind-service my-app-name my-elastic-apm-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 can be configured by modifying the [`config/elastic_apm_agent.yml`][] file in the buildpack fork. The framework uses the [`Repository` utility support][repositories] and so it supports the [version syntax][] defined there.
+
+| Name | Description
+| ---- | -----------
+| `service_name` | This can be overridden by a `service_name` entry in the credentials payload. If neither are supplied the default is the application_name as specified by Cloud Foundry.
+| `repository_root` | The URL of the Elastic APM repository index ([details][repositories]).
+| `version` | The version of Elastic APM to use. Candidate versions can be found in [this listing][].
+
+
+[Configuration and Extension]: ../README.md#configuration-and-extension
+[`config/elastic_apm_agent.yml`]: ../config/elastic_apm_agent.yml
+[Elastic APM]: https://www.elastic.co/guide/en/apm/agent/java/current/index.html
+[repositories]: extending-repositories.md
+[this listing]: https://raw.githubusercontent.com/elastic/apm-agent-java/master/cloudfoundry/index.yml
+[version syntax]: extending-repositories.md#version-syntax-and-ordering
+[Configuration of Elastic Agent]: https://www.elastic.co/guide/en/apm/agent/java/current/configuration.html
diff --git a/lib/java_buildpack/framework/elastic_apm_agent.rb b/lib/java_buildpack/framework/elastic_apm_agent.rb
new file mode 100644
index 0000000000..2c09a92b43
--- /dev/null
+++ b/lib/java_buildpack/framework/elastic_apm_agent.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+# Cloud Foundry Java Buildpack
+# Copyright 2013-2018 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.
+
+require 'java_buildpack/component/versioned_dependency_component'
+require 'java_buildpack/framework'
+
+module JavaBuildpack
+ module Framework
+
+ # Encapsulates the functionality for enabling zero-touch Elastic APM support.
+ class ElasticApmAgent < JavaBuildpack::Component::VersionedDependencyComponent
+
+ # (see JavaBuildpack::Component::BaseComponent#compile)
+ def compile
+ download_jar
+ @droplet.copy_resources
+ end
+
+ # Modifies the application's runtime configuration. The component is expected to transform members of the
+ # +context+ # (e.g. +@java_home+, +@java_opts+, etc.) in whatever way is necessary to support the function of the
+ # component.
+ #
+ # Container components are also expected to create the command required to run the application. These components
+ # are expected to read the +context+ values and take them into account when creating the command.
+ #
+ # @return [void, String] components other than containers and JREs are not expected to return any value.
+ # Container and JRE components are expected to return a command required to run the
+ # application.
+ # (see JavaBuildpack::Component::BaseComponent#release)
+ def release
+ credentials = @application.services.find_service(FILTER, [SERVER_URL, SECRET_TOKEN])['credentials']
+ java_opts = @droplet.java_opts
+ configuration = {}
+
+ apply_configuration(credentials, configuration)
+ apply_user_configuration(credentials, configuration)
+ write_java_opts(java_opts, configuration)
+
+ java_opts.add_javaagent(@droplet.sandbox + jar_name)
+ .add_system_property('elastic.apm.home', @droplet.sandbox)
+ end
+
+ protected
+
+ # (see JavaBuildpack::Component::VersionedDependencyComponent#supports?)
+ def supports?
+ @application.services.one_service? FILTER, [SERVER_URL, SECRET_TOKEN]
+ end
+
+ private
+
+ FILTER = /elastic[-]?apm/
+
+ BASE_KEY = 'elastic.apm.'
+
+ SERVER_URL = 'server_urls'
+
+ SECRET_TOKEN = "secret_token"
+
+ SERVICE_NAME = 'service_name'
+
+ private_constant :FILTER, :SERVER_URL, :BASE_KEY, :SECRET_TOKEN
+
+ def apply_configuration(credentials, configuration)
+ configuration['log_file_name'] = 'STDOUT'
+ configuration[SERVER_URL] = credentials[SERVER_URL]
+ configuration[SECRET_TOKEN] = credentials[SECRET_TOKEN]
+ configuration[SERVICE_NAME] = @application.details['application_name']
+ end
+
+ def apply_user_configuration(credentials, configuration)
+ credentials.each do |key, value|
+ configuration[key] = value
+ end
+ end
+
+ def write_java_opts(java_opts, configuration)
+ configuration.each do |key, value|
+ java_opts.add_system_property("elastic.apm.#{key}", value)
+ end
+ end
+
+ end
+ end
+end
diff --git a/rakelib/versions_task.rb b/rakelib/versions_task.rb
index 9cc89e2ec8..878409ed69 100644
--- a/rakelib/versions_task.rb
+++ b/rakelib/versions_task.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# Cloud Foundry Java Buildpack
-# Copyright 2013-2018 the original author or authors.
+# Copyright 2013-2019 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.
@@ -46,54 +46,55 @@ def initialize
private
- ARCHITECTURE_PATTERN = /\{architecture\}/
+ ARCHITECTURE_PATTERN = /\{architecture\}/.freeze
- DEFAULT_REPOSITORY_ROOT_PATTERN = /\{default.repository.root\}/
+ DEFAULT_REPOSITORY_ROOT_PATTERN = /\{default.repository.root\}/.freeze
NAME_MAPPINGS = {
- 'access_logging_support' => 'Tomcat Access Logging Support',
- 'agent' => 'Java Memory Assistant Agent',
- 'app_dynamics_agent' => 'AppDynamics Agent',
+ 'access_logging_support' => 'Tomcat Access Logging Support',
+ 'agent' => 'Java Memory Assistant Agent',
+ 'app_dynamics_agent' => 'AppDynamics Agent',
'azure_application_insights_agent' => 'Azure Application Insights Agent',
- 'clean_up' => 'Java Memory Assistant Clean Up',
- 'client_certificate_mapper' => 'Client Certificate Mapper',
- 'container_customizer' => 'Spring Boot Container Customizer',
- 'container_security_provider' => 'Container Security Provider',
- 'contrast_security_agent' => 'Contrast Security Agent',
- 'dyadic_ekm_security_provider' => 'Dyadic EKM Security Provider',
- 'dynatrace_appmon_agent' => 'Dynatrace Appmon Agent',
- 'dynatrace_one_agent' => 'Dynatrace OneAgent',
- 'geode_store' => 'Geode Tomcat Session Store',
- 'google_stackdriver_debugger' => 'Google Stackdriver Debugger',
- 'google_stackdriver_profiler' => 'Google Stackdriver Profiler',
- 'groovy' => 'Groovy',
- 'introscope_agent' => 'CA Introscope APM Framework',
- 'jacoco_agent' => 'JaCoCo Agent',
- 'jprofiler_profiler' => 'JProfiler Profiler',
- 'jre' => 'OpenJDK JRE',
- 'jre-11' => 'OpenJDK JRE 11',
- 'jrebel_agent' => 'JRebel Agent',
- 'jvmkill_agent' => 'jvmkill Agent',
- 'lifecycle_support' => 'Tomcat Lifecycle Support',
- 'logging_support' => 'Tomcat Logging Support',
- 'luna_security_provider' => 'Gemalto Luna Security Provider',
- 'maria_db_jdbc' => 'MariaDB JDBC Driver',
- 'memory_calculator' => 'Memory Calculator',
- 'metric_writer' => 'Metric Writer',
- 'new_relic_agent' => 'New Relic Agent',
- 'postgresql_jdbc' => 'PostgreSQL JDBC Driver',
- 'protect_app_security_provider' => 'Gemalto ProtectApp Security Provider',
- 'redis_store' => 'Redis Session Store',
- 'riverbed_appinternals_agent' => 'Riverbed Appinternals Agent',
- 'sky_walking_agent' => 'SkyWalking',
- 'spring_auto_reconfiguration' => 'Spring Auto-reconfiguration',
- 'spring_boot_cli' => 'Spring Boot CLI',
- 'takipi_agent' => 'Takipi Agent',
- 'tomcat' => 'Tomcat',
- 'your_kit_profiler' => 'YourKit Profiler'
+ 'clean_up' => 'Java Memory Assistant Clean Up',
+ 'client_certificate_mapper' => 'Client Certificate Mapper',
+ 'container_customizer' => 'Spring Boot Container Customizer',
+ 'container_security_provider' => 'Container Security Provider',
+ 'contrast_security_agent' => 'Contrast Security Agent',
+ 'dyadic_ekm_security_provider' => 'Dyadic EKM Security Provider',
+ 'dynatrace_appmon_agent' => 'Dynatrace Appmon Agent',
+ 'dynatrace_one_agent' => 'Dynatrace OneAgent',
+ 'elastic_apm_agent' => 'Elastic APM Agent',
+ 'geode_store' => 'Geode Tomcat Session Store',
+ 'google_stackdriver_debugger' => 'Google Stackdriver Debugger',
+ 'google_stackdriver_profiler' => 'Google Stackdriver Profiler',
+ 'groovy' => 'Groovy',
+ 'introscope_agent' => 'CA Introscope APM Framework',
+ 'jacoco_agent' => 'JaCoCo Agent',
+ 'jprofiler_profiler' => 'JProfiler Profiler',
+ 'jre' => 'OpenJDK JRE',
+ 'jre-11' => 'OpenJDK JRE 11',
+ 'jrebel_agent' => 'JRebel Agent',
+ 'jvmkill_agent' => 'jvmkill Agent',
+ 'lifecycle_support' => 'Tomcat Lifecycle Support',
+ 'logging_support' => 'Tomcat Logging Support',
+ 'luna_security_provider' => 'Gemalto Luna Security Provider',
+ 'maria_db_jdbc' => 'MariaDB JDBC Driver',
+ 'memory_calculator' => 'Memory Calculator',
+ 'metric_writer' => 'Metric Writer',
+ 'new_relic_agent' => 'New Relic Agent',
+ 'postgresql_jdbc' => 'PostgreSQL JDBC Driver',
+ 'protect_app_security_provider' => 'Gemalto ProtectApp Security Provider',
+ 'redis_store' => 'Redis Session Store',
+ 'riverbed_appinternals_agent' => 'Riverbed Appinternals Agent',
+ 'sky_walking_agent' => 'SkyWalking',
+ 'spring_auto_reconfiguration' => 'Spring Auto-reconfiguration',
+ 'spring_boot_cli' => 'Spring Boot CLI',
+ 'takipi_agent' => 'Takipi Agent',
+ 'tomcat' => 'Tomcat',
+ 'your_kit_profiler' => 'YourKit Profiler'
}.freeze
- PLATFORM_PATTERN = /\{platform\}/
+ PLATFORM_PATTERN = /\{platform\}/.freeze
private_constant :ARCHITECTURE_PATTERN, :DEFAULT_REPOSITORY_ROOT_PATTERN, :NAME_MAPPINGS,
:PLATFORM_PATTERN
@@ -103,8 +104,8 @@ def augment(raw, key, pattern, candidates, &block)
raw.map(&block)
elsif raw[:uri] =~ pattern
candidates.map do |candidate|
- dup = raw.clone
- dup[key] = candidate
+ dup = raw.clone
+ dup[key] = candidate
dup[:uri] = raw[:uri].gsub pattern, candidate
dup
@@ -153,7 +154,7 @@ def configurations(component_id, configuration, sub_component_id = nil)
configurations = []
if repository_configuration?(configuration)
- configuration['component_id'] = component_id
+ configuration['component_id'] = component_id
configuration['sub_component_id'] = sub_component_id if sub_component_id
if component_id == 'open_jdk_jre' && sub_component_id == 'jre'
@@ -178,7 +179,7 @@ def default_repository_root
def get_from_cache(cache, configuration, index_configuration)
cache.get(index_configuration[:uri]) do |f|
- index = YAML.safe_load f
+ index = YAML.safe_load f
found_version = version(configuration, index)
if found_version.nil?
@@ -193,7 +194,7 @@ def get_from_cache(cache, configuration, index_configuration)
def dependency_versions
dependency_versions = []
- cache = JavaBuildpack::Util::Cache::DownloadCache.new
+ cache = JavaBuildpack::Util::Cache::DownloadCache.new
configurations = component_ids.map { |component_id| component_configuration(component_id) }.flatten
configurations.each do |configuration|
@@ -206,9 +207,9 @@ def dependency_versions
raise "Unable to resolve name for '#{id}'" unless name
dependency_versions << {
- 'id' => id,
- 'name' => name,
- 'uri' => uri,
+ 'id' => id,
+ 'name' => name,
+ 'uri' => uri,
'version' => version
}
end
@@ -280,7 +281,7 @@ def version_yaml_task
def versions
{
- 'buildpack' => Package.version,
+ 'buildpack' => Package.version,
'dependencies' => dependency_versions
}
end
diff --git a/spec/fixtures/stub-elastic-apm-agent.jar b/spec/fixtures/stub-elastic-apm-agent.jar
new file mode 100644
index 0000000000..c7a27a109d
Binary files /dev/null and b/spec/fixtures/stub-elastic-apm-agent.jar differ
diff --git a/spec/java_buildpack/framework/elastic_apm_agent_spec.rb b/spec/java_buildpack/framework/elastic_apm_agent_spec.rb
new file mode 100644
index 0000000000..0014048ef8
--- /dev/null
+++ b/spec/java_buildpack/framework/elastic_apm_agent_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+# Cloud Foundry Java Buildpack
+# Copyright 2013-2018 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.
+
+require 'spec_helper'
+require 'component_helper'
+require 'java_buildpack/framework/new_relic_agent'
+require 'java_buildpack/util/tokenized_version'
+
+describe JavaBuildpack::Framework::ElasticApmAgent do
+ include_context 'with component help'
+
+ it 'does not detect without elastic-apm-n/a service' do
+ expect(component.detect).to be_nil
+ end
+
+ context do
+
+ before do
+ allow(services).to receive(:one_service?).with(/elastic[-]?apm/, %w[secret_token secret_token]).and_return(true)
+ end
+
+ it 'detects with elastic-apm-n/a service' do
+ expect(component.detect).to eq("elastic-apm-agent=#{version}")
+ end
+
+ it 'downloads elastic-apm agent JAR',
+ cache_fixture: 'stub-elastic-apm-agent.jar' do
+
+ component.compile
+
+ expect(sandbox + "elastic_apm_agent-#{version}.jar").to exist
+ end
+
+ it 'copies resources',
+ cache_fixture: 'stub-elastic-apm-agent.jar' do
+
+ component.compile
+
+ expect(sandbox + 'elastic.yml').to exist
+ end
+
+ it 'updates JAVA_OPTS' do
+ allow(services).to receive(:find_service).and_return('credentials' => { 'secret_token' => 'secret_token' })
+ allow(java_home).to receive(:java_8_or_later?).and_return(JavaBuildpack::Util::TokenizedVersion.new('1.4.0'))
+
+ component.release
+
+ expect(java_opts).to include("-javaagent:$PWD/.java-buildpack/elastic_apm_agent/elastic_apm_agent-#{version}.jar")
+ expect(java_opts).to include('-Delastic.apm.home=$PWD/.java-buildpack/elastic_apm_agent')
+ expect(java_opts).to include('-Delastic.apm.config.secret_token=test-license-key')
+ expect(java_opts).to include('-Delastic.apm.config.app_name=test-application-name')
+ expect(java_opts).to include('-Delastic.apm.config.log_file_name=STDOUT')
+ end
+
+ it 'updates JAVA_OPTS with additional options' do
+ allow(services).to receive(:find_service).and_return('credentials' => { 'secret_token' => 'test-secret_token',
+ 'server_urls' => 'different-serverurl',
+ 'service_name' => 'different-name',
+ 'foo' => 'bar' })
+ allow(java_home).to receive(:java_8_or_later?).and_return(JavaBuildpack::Util::TokenizedVersion.new('1.4.0'))
+
+ component.release
+
+ expect(java_opts).to include('-Dnelastic.apm.config.secret_token=test-secret_token')
+ expect(java_opts).to include('-Delastic.apm.server_urls=different-serverurl')
+ expect(java_opts).to include('-Delastic.apm.service_name=different-name')
+ expect(java_opts).to include('-Delastic.apm.foo=bar')
+ end
+
+ it 'updates JAVA_OPTS on Java 8' do
+ allow(services).to receive(:find_service).and_return('credentials' => { 'secret_token' => 'test-license-key' })
+ allow(java_home).to receive(:java_8_or_later?).and_return(JavaBuildpack::Util::TokenizedVersion.new('1.4.0'))
+
+ component.release
+
+ expect(java_opts).to include('-Delastic.apm.enable.java.8=true')
+ end
+
+ end
+
+end