From c10ed0d38a149c5cbf13f0303ba113f1651b5372 Mon Sep 17 00:00:00 2001 From: Ekrem Sekerci Date: Fri, 25 Nov 2022 14:45:17 +0100 Subject: [PATCH 1/3] Enable Dynatrace through telegraf --- README.md | 13 ++-- buildpack/stage.py | 2 - buildpack/start.py | 2 - buildpack/telemetry/dynatrace.py | 117 +++++-------------------------- buildpack/telemetry/metrics.py | 3 +- buildpack/telemetry/telegraf.py | 9 ++- dependencies.yml | 4 -- etc/telegraf/telegraf.toml.j2 | 19 ++++- tests/unit/test_dynatrace.py | 24 +++++++ 9 files changed, 73 insertions(+), 120 deletions(-) create mode 100644 tests/unit/test_dynatrace.py diff --git a/README.md b/README.md index 9b211b668..52d4e6cd3 100644 --- a/README.md +++ b/README.md @@ -724,15 +724,12 @@ The most important metrics ( `before_xid_wraparound` , `connections` , `database To enable Dynatrace, configure the following environment variables: -| Environment Variable | Description | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `DT_PAAS_TOKEN` | The token for integrating your Dynatrace environment with Cloud Foundry. You can find it in the deploy Dynatrace section within your environment. | -| `DT_SAAS_URL` | Monitoring endpoint url of the Dynatrace service | -| `DT_TENANT` | Your Dynatrace environment ID is the unique identifier of your Dynatrace environment. You can find it in the deploy Dynatrace section within your environment. | +| Environment Variable | Description | +| -------------------- | ----------------------------------------------------------------------------------------------| +| `DT_PAAS_TOKEN` | The token for integrating your Dynatrace environment with your Mendix app | +| `DT_SAAS_URL` | Monitoring endpoint url of the Dynatrace service | -By setting these environment variables automatically the Dynatrace OneAgent will be loaded in the container. - -OneAgent will be able to measure all J2EE related Metrics from the Application. See OneAgent documention for more details. +By setting these environment variables, ingestion of metrics to Dynatrace will start. ### Logging The buildpack provides several options to configure logging. diff --git a/buildpack/stage.py b/buildpack/stage.py index 048634e5b..f9a09742c 100755 --- a/buildpack/stage.py +++ b/buildpack/stage.py @@ -10,7 +10,6 @@ from buildpack.telemetry import ( appdynamics, datadog, - dynatrace, fluentbit, splunk, logs, @@ -205,7 +204,6 @@ def cleanup_dependency_cache(cached_dir, dependency_list): java_version, ) appdynamics.stage(BUILDPACK_DIR, DOT_LOCAL_LOCATION, CACHE_DIR) - dynatrace.stage(BUILDPACK_DIR, DOT_LOCAL_LOCATION, CACHE_DIR) splunk.stage() fluentbit.stage(BUILDPACK_DIR, DOT_LOCAL_LOCATION, CACHE_DIR) newrelic.stage(BUILDPACK_DIR, DOT_LOCAL_LOCATION, CACHE_DIR) diff --git a/buildpack/start.py b/buildpack/start.py index 4bc3897b7..7b61d9476 100755 --- a/buildpack/start.py +++ b/buildpack/start.py @@ -13,7 +13,6 @@ from buildpack.telemetry import ( appdynamics, datadog, - dynatrace, fluentbit, splunk, logs, @@ -149,7 +148,6 @@ def _register_signal_handlers(): java.update_config(m2ee, application_name, util.get_vcap_data(), runtime_version) newrelic.update_config(m2ee, application_name) appdynamics.update_config(m2ee) - dynatrace.update_config(m2ee, application_name) splunk.update_config(m2ee) fluentbit.update_config(m2ee) mx_java_agent.update_config(m2ee) diff --git a/buildpack/telemetry/dynatrace.py b/buildpack/telemetry/dynatrace.py index 45e0e2759..4fe939dcc 100644 --- a/buildpack/telemetry/dynatrace.py +++ b/buildpack/telemetry/dynatrace.py @@ -1,110 +1,25 @@ +""" +For Dynatrace, metrics are directly ingested through telegraf. +No additional setup is needed. +This module only collects information for telegraf from environment variables. +""" import logging import os -import shutil -import json +from urllib.parse import urljoin -from buildpack import util +INGEST_ENDPOINT = "/api/v2/metrics/ingest" -default_env = { - "DT_TENANTTOKEN": None, # optional, default to one found in manifest.json - "DT_APPLICATIONID": None, # optional, default not set - "DT_TENANT": None, # required, environment ID (uuid format) - # "DT_PAAS_TOKEN": None, # required - # "DT_SAAS_URL": None, # required - "DT_LOGSTREAM": "stdout", - "DT_NETWORK_ZONE": None, # optional, not sure what this means :D - "DT_CUSTOM_PROP": None, # optional metadata e.g. Department=Acceptance Stage=Sprint - "DT_TAGS": None, # optional tags e.g. MikesStuff easyTravel=Mike -} - -def stage(buildpack_dir, build_path, cache_path): - if is_enabled(): - try: - util.resolve_dependency( - "dynatrace.agent", - build_path, # DOT_LOCAL_LOCATION, - buildpack_dir=buildpack_dir, - cache_dir=cache_path, # CACHE_DIR, - unpack=True, - overrides={ - "url": os.environ.get("DT_SAAS_URL"), - "environment": os.environ.get("DT_TENANT"), - "token": os.environ.get("DT_PAAS_TOKEN"), - }, - ignore_cache=True - ) - except Exception as e: - logging.warning( - "Dynatrace agent download and unpack failed", exc_info=True - ) +def is_enabled(): + return "DT_PAAS_TOKEN" in os.environ.keys() and \ + "DT_SAAS_URL" in os.environ.keys() -def update_config(m2ee, app_name): +def get_ingestion_info(): if not is_enabled(): - logging.debug( - "Skipping Dynatrace setup, no DT_TENANTTOKEN found in environment" - ) - return - logging.info("Enabling Dynatrace") - try: - manifest = get_manifest() - except Exception as e: - logging.warning( - "Failed to parse Dynatrace manifest file", exc_info=True - ) - return - - # dynamic default - default_env.update({"DT_TENANTTOKEN": manifest.get("tenantToken")}) - - for key, dv in default_env.items(): - value = os.environ.get(key, dv) - if value: - util.upsert_custom_environment_variable(m2ee, key, value) - util.upsert_custom_environment_variable( - m2ee, "DT_CONNECTION_POINT", get_connection_endpoint() - ) - - agent_path = os.path.join(".local", get_agent_path()) - if not os.path.exists(agent_path): - raise Exception( - "Dynatrace Agent not found: {agent_path}".format(agent_path) - ) - - util.upsert_javaopts( - m2ee, - [ - "-agentpath:{path}".format(path=os.path.abspath(agent_path)), - "-Xshare:off", - ], - ) - + return None, None -def get_manifest(): - with open(".local/manifest.json", "r") as f: - return json.load(f) - - -def get_connection_endpoint(): - manifest = get_manifest() - endpoints = manifest.get("communicationEndpoints", []) - # prepend the DT_SAAS_URL because the communication endpoints might not be correct - endpoints.insert( - 0, "{url}/communication".format(url=os.environ.get("DT_SAAS_URL")) - ) - return ";".join(endpoints) - - -def get_agent_path(): - manifest = get_manifest() - technologies = manifest.get("technologies") - java_binaries = technologies.get("java").get("linux-x86-64") - for f in java_binaries: - binary_type = f.get("binarytype") - if binary_type == "loader": - return f.get("path") - - -def is_enabled(): - return "DT_PAAS_TOKEN" in os.environ.keys() + logging.info("Metrics ingestion to Dynatrace is configured") + token = os.getenv("DT_PAAS_TOKEN") + ingest_url = urljoin(os.getenv("DT_SAAS_URL"), INGEST_ENDPOINT) + return token, ingest_url diff --git a/buildpack/telemetry/metrics.py b/buildpack/telemetry/metrics.py index f47f9c257..546e75043 100644 --- a/buildpack/telemetry/metrics.py +++ b/buildpack/telemetry/metrics.py @@ -18,7 +18,7 @@ from lib.m2ee import munin from lib.m2ee.version import MXVersion -from . import datadog, appdynamics +from . import datadog, appdynamics, dynatrace # Runtime configuration for influx registry # This enables the new stream of metrics coming from micrometer instead @@ -200,6 +200,7 @@ def configure_metrics_registry(m2ee): datadog.is_enabled() or get_appmetrics_target() or appdynamics.machine_agent_enabled() + or dynatrace.is_enabled() ): paidapps_registries.append(STATSD_REGISTRY) diff --git a/buildpack/telemetry/telegraf.py b/buildpack/telemetry/telegraf.py index b2ea137a5..f55581c0f 100644 --- a/buildpack/telemetry/telegraf.py +++ b/buildpack/telemetry/telegraf.py @@ -17,7 +17,7 @@ from buildpack.infrastructure import database from jinja2 import Template -from . import datadog, metrics, mx_java_agent, appdynamics +from . import datadog, metrics, mx_java_agent, appdynamics, dynatrace NAMESPACE = "telegraf" DEPENDENCY = "%s.agent" % NAMESPACE @@ -91,6 +91,7 @@ def include_db_metrics(): is_appmetrics or datadog.is_enabled() or appdynamics.machine_agent_enabled() + or dynatrace.is_enabled() ): # For customers who have Datadog or AppDynamics or APPMETRICS_TARGET enabled, # we always include the database metrics. They can opt out @@ -109,6 +110,7 @@ def is_enabled(runtime_version): metrics.get_appmetrics_target() is not None or datadog.is_enabled() or appdynamics.machine_agent_enabled() + or dynatrace.is_enabled() or metrics.micrometer_metrics_enabled(runtime_version) ) @@ -231,6 +233,8 @@ def update_config(m2ee, app_name): # app and / or service tag not set tags["service"] = datadog.get_service_tag() + dynatrace_token, dynatrace_ingest_url = dynatrace.get_ingestion_info() + with open(template_path, "r") as file_: template = Template(file_.read(), trim_blocks=True, lstrip_blocks=True) rendered = template.render( @@ -252,6 +256,9 @@ def update_config(m2ee, app_name): # For Telegraf config only AppDynamics Machine Agent makes sense. appdynamics_enabled=appdynamics.machine_agent_enabled(), appdynamics_output_script_path=APPDYNAMICS_OUTPUT_SCRIPT_PATH, + dynatrace_enabled=dynatrace.is_enabled(), + dynatrace_ingest_url=dynatrace_ingest_url, + dynatrace_token=dynatrace_token, ) logging.debug("Writing Telegraf configuration file...") diff --git a/dependencies.yml b/dependencies.yml index 5c2c58852..a23a26327 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -24,10 +24,6 @@ dependencies: trace-agent: artifact: datadog/dd-java-agent-{{ version }}.jar version: 0.101.0 - dynatrace: - agent: - artifact: "{{ url }}/e/{{ environment }}/api/v1/deployment/installer/agent/unix/paas/latest?include=java&bitness=64&Api-Token={{ token }}" - managed: false fluentbit: artifact: fluentbit/fluent-bit-{{ version }}.tar.gz version: 1.9.2 diff --git a/etc/telegraf/telegraf.toml.j2 b/etc/telegraf/telegraf.toml.j2 index c49a164bc..61c85894e 100644 --- a/etc/telegraf/telegraf.toml.j2 +++ b/etc/telegraf/telegraf.toml.j2 @@ -39,7 +39,7 @@ {% endif %} {% if db_config %} -{% if not (datadog_api_key or appdynamics_enabled) %} +{% if not (datadog_api_key or appdynamics_enabled or dynatrace_enabled) %} # PostgreSQL input (standard) [[inputs.postgresql]] address = "postgres://{{ db_config['DatabaseUserName'] }}:{{ db_config['DatabasePassword'] }}@{{ db_config['DatabaseHost'] }}/{{ db_config['DatabaseName'] }}" @@ -297,6 +297,23 @@ micrometer_metrics = ["true"] {% endif %} +{% if dynatrace_enabled %} +[[outputs.dynatrace]] +# Your Dynatrace environment URL. +# For Dynatrace SaaS environments the URL scheme is "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest" +# For Dynatrace Managed environments the URL scheme is "https://{your-domain}/e/{your-environment-id}/api/v2/metrics/ingest" + url = "{{ dynatrace_ingest_url }}" +# +# Your Dynatrace API token. +# Create an API token within your Dynatrace environment, by navigating to Access Tokens > Generate new token +# The API token needs "Ingest metrics" scope permission. + api_token = "{{ dynatrace_token }}" +# +# Ignore any micrometer_metrics + [outputs.dynatrace.tagdrop] + micrometer_metrics = ["true"] +{% endif %} + {% if micrometer_metrics %} #################################################################################### # App metrics via micrometer # diff --git a/tests/unit/test_dynatrace.py b/tests/unit/test_dynatrace.py new file mode 100644 index 000000000..6809d0536 --- /dev/null +++ b/tests/unit/test_dynatrace.py @@ -0,0 +1,24 @@ +import os +from unittest.mock import patch +from urllib.parse import urljoin + +from buildpack.telemetry import dynatrace +from unittest import TestCase + + +class TestDynatrace(TestCase): + def test_is_enabled_false(self): + # Should be False without necessary environment variables + self.assertFalse(dynatrace.is_enabled()) + + def test_get_ingestion_info(self): + # for get_ingestion_info(), also covers enabled case of is_enabled() + token = "DUMMY_TOKEN" + url = "DUMMY_URL" + expected_ingest_url = urljoin(url, dynatrace.INGEST_ENDPOINT) + env_vars = {"DT_PAAS_TOKEN": token, "DT_SAAS_URL": url} + with patch.dict(os.environ, env_vars): + actual_info = dynatrace.get_ingestion_info() + + expected_info = (token, expected_ingest_url) + self.assertEqual(expected_info, actual_info) From 6716c13de8f5a9778ec69fde7374c91ff937c54e Mon Sep 17 00:00:00 2001 From: Ekrem Sekerci Date: Fri, 9 Dec 2022 15:02:40 +0100 Subject: [PATCH 2/3] Upgrade telegraf version to 1.24.4 Also start pushing default dimensions to Dynatrace --- buildpack/telemetry/telegraf.py | 1 + dependencies.yml | 2 +- etc/telegraf/telegraf.toml.j2 | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/buildpack/telemetry/telegraf.py b/buildpack/telemetry/telegraf.py index f55581c0f..7b1f10dbc 100644 --- a/buildpack/telemetry/telegraf.py +++ b/buildpack/telemetry/telegraf.py @@ -259,6 +259,7 @@ def update_config(m2ee, app_name): dynatrace_enabled=dynatrace.is_enabled(), dynatrace_ingest_url=dynatrace_ingest_url, dynatrace_token=dynatrace_token, + dynatrace_env=os.getenv('DT_ENV'), ) logging.debug("Writing Telegraf configuration file...") diff --git a/dependencies.yml b/dependencies.yml index a23a26327..861c9ccaa 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -89,4 +89,4 @@ dependencies: telegraf: agent: artifact: telegraf/telegraf-{{ version }}_linux_amd64.tar.gz - version: 1.16.3 + version: 1.24.4 diff --git a/etc/telegraf/telegraf.toml.j2 b/etc/telegraf/telegraf.toml.j2 index 61c85894e..0166b04ab 100644 --- a/etc/telegraf/telegraf.toml.j2 +++ b/etc/telegraf/telegraf.toml.j2 @@ -312,6 +312,14 @@ # Ignore any micrometer_metrics [outputs.dynatrace.tagdrop] micrometer_metrics = ["true"] +# +# Optional dimensions to be added to every metric + [outputs.dynatrace.default_dimensions] + app = "{{ app_name }}" + instance_index = "{{ cf_instance_index }}" + {% if dynatrace_env %} + env = "{{ dynatrace_env }}" + {% endif %} {% endif %} {% if micrometer_metrics %} From ea24fd45bdf6f74eb030d8a0f37a3b6a2f55a2f7 Mon Sep 17 00:00:00 2001 From: Ekrem Sekerci Date: Mon, 19 Dec 2022 15:43:12 +0100 Subject: [PATCH 3/3] Get extra tags for Dynatrace from environment variable TAGS --- README.md | 10 +++++++--- buildpack/telemetry/telegraf.py | 18 +++++++++++++++--- etc/telegraf/telegraf.toml.j2 | 12 +++++------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 52d4e6cd3..2fe6e807d 100644 --- a/README.md +++ b/README.md @@ -720,8 +720,7 @@ The most important metrics ( `before_xid_wraparound` , `connections` , `database ### Dynatrace -[Dynatrace SaaS/Managed](http://www.dynatrace.com/cloud-foundry/) is your full stack monitoring solution - powered by artificial intelligence. Dynatrace SaaS/Managed allows you insights into all application requests from the users click in the browser down to the database statement and code-level. - +Dynatrace integration is supported in Mendix version 9.7 and above. To enable Dynatrace, configure the following environment variables: | Environment Variable | Description | @@ -729,7 +728,12 @@ To enable Dynatrace, configure the following environment variables: | `DT_PAAS_TOKEN` | The token for integrating your Dynatrace environment with your Mendix app | | `DT_SAAS_URL` | Monitoring endpoint url of the Dynatrace service | -By setting these environment variables, ingestion of metrics to Dynatrace will start. +Buildpack attaches these default tags to metrics that are pushed to Dynatrace: + +* `app` - Environment Id of the Mendix application +* `instance_index` - Instance index that metrics belong to + +Extra tags can be attached via `TAGS` environment variable (example value: `[env:accp]`) ### Logging The buildpack provides several options to configure logging. diff --git a/buildpack/telemetry/telegraf.py b/buildpack/telemetry/telegraf.py index 7b1f10dbc..e03753da6 100644 --- a/buildpack/telemetry/telegraf.py +++ b/buildpack/telemetry/telegraf.py @@ -184,6 +184,20 @@ def _get_db_config(): return None +def _get_dynatrace_config(app_name): + token, ingest_url = dynatrace.get_ingestion_info() + tags = util.get_tags() + return { + "token": token, + "ingest_url": ingest_url, + "dimensions": { + "app": app_name, + "instance_index": _get_app_index(), + **tags + } + } + + def _fix_metrics_registries_config(m2ee): # Metrics.Registries is a nested JSON entry. As a result, when we read # from the environment, it is not a list of registries but a string. @@ -257,9 +271,7 @@ def update_config(m2ee, app_name): appdynamics_enabled=appdynamics.machine_agent_enabled(), appdynamics_output_script_path=APPDYNAMICS_OUTPUT_SCRIPT_PATH, dynatrace_enabled=dynatrace.is_enabled(), - dynatrace_ingest_url=dynatrace_ingest_url, - dynatrace_token=dynatrace_token, - dynatrace_env=os.getenv('DT_ENV'), + dynatrace_config=_get_dynatrace_config(app_name), ) logging.debug("Writing Telegraf configuration file...") diff --git a/etc/telegraf/telegraf.toml.j2 b/etc/telegraf/telegraf.toml.j2 index 0166b04ab..5fea39eb7 100644 --- a/etc/telegraf/telegraf.toml.j2 +++ b/etc/telegraf/telegraf.toml.j2 @@ -302,12 +302,12 @@ # Your Dynatrace environment URL. # For Dynatrace SaaS environments the URL scheme is "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest" # For Dynatrace Managed environments the URL scheme is "https://{your-domain}/e/{your-environment-id}/api/v2/metrics/ingest" - url = "{{ dynatrace_ingest_url }}" + url = "{{ dynatrace_config['ingest_url'] }}" # # Your Dynatrace API token. # Create an API token within your Dynatrace environment, by navigating to Access Tokens > Generate new token # The API token needs "Ingest metrics" scope permission. - api_token = "{{ dynatrace_token }}" + api_token = "{{ dynatrace_config['token'] }}" # # Ignore any micrometer_metrics [outputs.dynatrace.tagdrop] @@ -315,11 +315,9 @@ # # Optional dimensions to be added to every metric [outputs.dynatrace.default_dimensions] - app = "{{ app_name }}" - instance_index = "{{ cf_instance_index }}" - {% if dynatrace_env %} - env = "{{ dynatrace_env }}" - {% endif %} + {% for key, value in dynatrace_config['dimensions'].items() %} + {{ key }} = "{{ value }}" + {% endfor %} {% endif %} {% if micrometer_metrics %}