reporterSupplier)
{
- Metric metric = metricsRegistry.get(metricName);
- if (metric == null)
+ try
{
- metric = getExistingOrAddNewMetric(metricName, new ValueMetric());
+ return Optional.ofNullable(reporterSupplier.call());
}
-
- metric.update(value);
- }
-
- /**
- * Gets a metric from the metrics registry, or if it does not exist yet, adds the given metric to the registry and
- * returns that one.
- *
- * @param metricName
- * the name of the metric
- * @param newMetric
- * the metric to add in case there is no such metric yet
- */
- private synchronized Metric getExistingOrAddNewMetric(final String metricName, final Metric newMetric)
- {
- Metric metric = metricsRegistry.get(metricName);
- if (metric == null)
+ catch (final Exception e)
{
- metric = newMetric;
- metricsRegistry.put(metricName, metric);
+ log.error("Failed to start metrics reporter '{}'", str, e);
}
- return metric;
- }
-
- /**
- * Sanitizes the given full metric name such that any invalid character is replaced with an underscore. Valid
- * characters are letters, numbers, the underscore, the minus sign, and the dot.
- *
- * @param metricName
- * the name of the metric
- * @return the sanitized metric name
- */
- private String sanitizeFullMetricName(final String metricName)
- {
- return sanitizeMetricName(metricName, true);
+ return Optional.empty();
}
- /**
- * Sanitizes the given metric name part such that any invalid character is replaced with an underscore. Valid
- * characters are letters, numbers, the underscore, and the minus sign.
- *
- * @param metricNamePart
- * one part of a full metric name
- * @return the sanitized metric name part
- */
- private String sanitizeMetricNamePart(final String metricNamePart)
- {
- return sanitizeMetricName(metricNamePart, false);
- }
-
- /**
- * Sanitizes the given metric name such that any invalid character is replaced with an underscore. Valid characters
- * are letters, numbers, the underscore, and the minus sign. Whether or not the dot is valid as well can be
- * controlled by the second parameter.
- *
- * Note that Graphite may allow more characters, but not always in all combinations (e.g., aaa[] is OK, while aaa[a]
- * is not). That's why we are somewhat over-restrictive here. Better safe than sorry.
- *
- * @param metricName
- * the name of the metric
- * @param dotsAllowed
- * whether a dot is also a valid character
- * @return the sanitized metric name
- */
- private String sanitizeMetricName(final String metricName, boolean dotsAllowed)
- {
- final char[] chars = metricName.toCharArray();
-
- for (int i = 0; i < chars.length; i++)
- {
- final char c = chars[i];
- final boolean isValidChar = CharUtils.isAsciiAlphanumeric(c) || c == '_' || c == '-' || (c == '.' && dotsAllowed);
-
- if (!isValidChar)
- {
- chars[i] = '_';
- }
- }
-
- return new String(chars);
- }
}
diff --git a/src/main/java/com/xceptance/xlt/engine/metrics/MetricsReporter.java b/src/main/java/com/xceptance/xlt/engine/metrics/MetricsReporter.java
new file mode 100644
index 000000000..5c5a3e3c1
--- /dev/null
+++ b/src/main/java/com/xceptance/xlt/engine/metrics/MetricsReporter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.xceptance.xlt.engine.metrics;
+
+import com.xceptance.xlt.api.engine.Data;
+
+public interface MetricsReporter
+{
+ public void reportMetrics(final Data data);
+}
diff --git a/src/main/java/com/xceptance/xlt/engine/metrics/graphite/GraphiteMetricsReporter.java b/src/main/java/com/xceptance/xlt/engine/metrics/graphite/GraphiteMetricsReporter.java
new file mode 100644
index 000000000..b3202d99c
--- /dev/null
+++ b/src/main/java/com/xceptance/xlt/engine/metrics/graphite/GraphiteMetricsReporter.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.xceptance.xlt.engine.metrics.graphite;
+
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.lang3.CharUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.xceptance.xlt.agent.JvmResourceUsageData;
+import com.xceptance.xlt.api.engine.ActionData;
+import com.xceptance.xlt.api.engine.CustomData;
+import com.xceptance.xlt.api.engine.Data;
+import com.xceptance.xlt.api.engine.EventData;
+import com.xceptance.xlt.api.engine.PageLoadTimingData;
+import com.xceptance.xlt.api.engine.RequestData;
+import com.xceptance.xlt.api.engine.Session;
+import com.xceptance.xlt.api.engine.TransactionData;
+import com.xceptance.xlt.engine.metrics.CounterMetric;
+import com.xceptance.xlt.engine.metrics.Metric;
+import com.xceptance.xlt.engine.metrics.MetricsReporter;
+import com.xceptance.xlt.engine.metrics.RateMetric;
+import com.xceptance.xlt.engine.metrics.ValueMetric;
+
+public class GraphiteMetricsReporter implements MetricsReporter
+{
+ private static final Logger log = LoggerFactory.getLogger(GraphiteMetricsReporter.class);
+
+ private static final int ONE_SEC = 1000;
+
+ private static final int ONE_HOUR = 3600 * ONE_SEC;
+
+ private final int reportingInterval;
+
+ /**
+ * The current agent's ID. All characters illegal for Graphite have been sanitized.
+ */
+ private final String sanitizedAgentId;
+
+ /**
+ * All the currently known metrics, keyed by metric name.
+ */
+ private final Map metricsRegistry = new ConcurrentHashMap();
+
+ public GraphiteMetricsReporter(final int interval, final String host, final int port, String metricsNamePrefix)
+ throws UnknownHostException, IllegalArgumentException
+ {
+ this.reportingInterval = interval * ONE_SEC;
+
+ // sanitize the metric name prefix and add a trailing dot if not present yet
+ metricsNamePrefix = sanitizeFullMetricName(metricsNamePrefix);
+ if (!metricsNamePrefix.endsWith("."))
+ {
+ metricsNamePrefix = metricsNamePrefix + ".";
+ }
+
+ // get and sanitize the current agent's ID now, we will need it often
+ sanitizedAgentId = sanitizeMetricNamePart(Session.getCurrent().getAgentID());
+
+ final PlainTextCarbonClient carbonClient = new PlainTextCarbonClient(host, port);
+ final GraphiteReporter reporter = new GraphiteReporter(carbonClient, metricsRegistry, metricsNamePrefix, reportingInterval);
+ reporter.start();
+
+ if (log.isInfoEnabled())
+ {
+ log.info(String.format("Started reporting metrics to Graphite server %s:%d every %d second(s)", host, port, interval));
+ }
+
+ }
+
+ public void reportMetrics(final Data data)
+ {
+ // do the right thing depending on the data type
+ if (data instanceof RequestData)
+ {
+ updateRequestMetrics((RequestData) data);
+ }
+ else if (data instanceof ActionData)
+ {
+ updateActionMetrics((ActionData) data);
+ }
+ else if (data instanceof TransactionData)
+ {
+ updateTransactionMetrics((TransactionData) data);
+ }
+ else if (data instanceof PageLoadTimingData)
+ {
+ updatePageLoadTimingMetrics((PageLoadTimingData) data);
+ }
+ else if (data instanceof CustomData)
+ {
+ updateCustomTimerMetrics((CustomData) data);
+ }
+ else if (data instanceof EventData)
+ {
+ updateEventMetrics((EventData) data);
+ }
+ else if (data instanceof JvmResourceUsageData)
+ {
+ updateJvmMetrics((JvmResourceUsageData) data);
+ }
+ }
+
+ private void updateTransactionMetrics(final TransactionData transactionData)
+ {
+ // metrics per transaction name
+ final String sanitizedName = sanitizeMetricNamePart(transactionData.getName());
+ final String metricPrefix = sanitizedAgentId + ".transactions." + sanitizedName + ".";
+
+ updateValueMetric(metricPrefix + "runtime", (int) transactionData.getRunTime());
+ updateCounterMetric(metricPrefix + "errors", transactionData.hasFailed() ? 1 : 0);
+ updateRateMetric(metricPrefix + "arrivals_1h", 1, ONE_HOUR, reportingInterval);
+
+ // summary metrics
+ final String summaryMetricPrefix = sanitizedAgentId + ".summary.transactions.";
+
+ updateValueMetric(summaryMetricPrefix + "runtime", (int) transactionData.getRunTime());
+ updateCounterMetric(summaryMetricPrefix + "count", 1);
+ updateCounterMetric(summaryMetricPrefix + "errors", transactionData.hasFailed() ? 1 : 0);
+ updateRateMetric(summaryMetricPrefix + "arrivals_1h", 1, ONE_HOUR, reportingInterval);
+ }
+
+ private void updateActionMetrics(final ActionData actionData)
+ {
+ // metrics per action name
+ final String sanitizedName = sanitizeMetricNamePart(actionData.getName());
+ final String metricPrefix = sanitizedAgentId + ".actions." + sanitizedName + ".";
+
+ updateValueMetric(metricPrefix + "runtime", (int) actionData.getRunTime());
+ updateCounterMetric(metricPrefix + "errors", actionData.hasFailed() ? 1 : 0);
+
+ // summary metrics
+ final String summaryMetricPrefix = sanitizedAgentId + ".summary.actions.";
+
+ updateValueMetric(summaryMetricPrefix + "runtime", (int) actionData.getRunTime());
+ updateCounterMetric(summaryMetricPrefix + "count", 1);
+ updateCounterMetric(summaryMetricPrefix + "errors", actionData.hasFailed() ? 1 : 0);
+ }
+
+ private void updateRequestMetrics(final RequestData requestData)
+ {
+ // first get rid of the sub request numbering ("Foo.1.1" -> "Foo")
+ final String strippedRequestName = StringUtils.substringBefore(requestData.getName(), ".");
+
+ // metrics per request name
+ final String sanitizedName = sanitizeMetricNamePart(strippedRequestName);
+ final String metricPrefix = sanitizedAgentId + ".requests." + sanitizedName + ".";
+
+ updateValueMetric(metricPrefix + "runtime", (int) requestData.getRunTime());
+ updateCounterMetric(metricPrefix + "errors", requestData.hasFailed() ? 1 : 0);
+
+ // summary metrics
+ final String summaryMetricPrefix = sanitizedAgentId + ".summary.requests.";
+
+ updateValueMetric(summaryMetricPrefix + "runtime", (int) requestData.getRunTime());
+ updateCounterMetric(summaryMetricPrefix + "count", 1);
+ updateCounterMetric(summaryMetricPrefix + "errors", requestData.hasFailed() ? 1 : 0);
+ updateRateMetric(summaryMetricPrefix + "bytesSent_1s", requestData.getBytesSent(), ONE_SEC, reportingInterval);
+ updateRateMetric(summaryMetricPrefix + "bytesReceived_1s", requestData.getBytesReceived(), ONE_SEC, reportingInterval);
+ }
+
+ private void updatePageLoadTimingMetrics(final PageLoadTimingData pageLoadTimingData)
+ {
+ // metrics per page load timing name
+ final String sanitizedName = sanitizeMetricNamePart(pageLoadTimingData.getName());
+ final String metricPrefix = sanitizedAgentId + ".pageLoadTimings." + sanitizedName + ".";
+
+ updateValueMetric(metricPrefix + "runtime", (int) pageLoadTimingData.getRunTime());
+ updateCounterMetric(metricPrefix + "errors", pageLoadTimingData.hasFailed() ? 1 : 0);
+
+ // summary metrics
+ final String summaryMetricPrefix = sanitizedAgentId + ".summary.pageLoadTimings.";
+
+ updateValueMetric(summaryMetricPrefix + "runtime", (int) pageLoadTimingData.getRunTime());
+ updateCounterMetric(summaryMetricPrefix + "count", 1);
+ updateCounterMetric(summaryMetricPrefix + "errors", pageLoadTimingData.hasFailed() ? 1 : 0);
+ }
+
+ private void updateCustomTimerMetrics(final CustomData customData)
+ {
+ // metrics per custom timer name
+ final String sanitizedName = sanitizeMetricNamePart(customData.getName());
+ final String metricPrefix = sanitizedAgentId + ".custom." + sanitizedName + ".";
+
+ updateValueMetric(metricPrefix + "runtime", (int) customData.getRunTime());
+ updateCounterMetric(metricPrefix + "errors", customData.hasFailed() ? 1 : 0);
+
+ // summary metrics
+ final String summaryMetricPrefix = sanitizedAgentId + ".summary.custom.";
+
+ updateValueMetric(summaryMetricPrefix + "runtime", (int) customData.getRunTime());
+ updateCounterMetric(summaryMetricPrefix + "count", 1);
+ updateCounterMetric(summaryMetricPrefix + "errors", customData.hasFailed() ? 1 : 0);
+ }
+
+ private void updateEventMetrics(final EventData eventData)
+ {
+ // summary metrics
+ final String summaryMetricPrefix = sanitizedAgentId + ".summary.events.";
+
+ updateCounterMetric(summaryMetricPrefix + "count", 1);
+ }
+
+ private void updateJvmMetrics(final JvmResourceUsageData jvmData)
+ {
+ // metrics per agent
+ final String metricPrefix = sanitizedAgentId + ".agent.";
+
+ updateValueMetric(metricPrefix + "heapUsage", (int) jvmData.getHeapUsage());
+ updateValueMetric(metricPrefix + "totalCpuUsage", (int) jvmData.getTotalCpuUsage());
+ }
+
+ /**
+ * Updates the counter metric with the given name.
+ *
+ * @param metricName
+ * the name of the metric
+ * @param value
+ * the value to add to the counter
+ */
+ private void updateCounterMetric(final String metricName, final int value)
+ {
+ Metric metric = metricsRegistry.get(metricName);
+ if (metric == null)
+ {
+ metric = getExistingOrAddNewMetric(metricName, new CounterMetric());
+ }
+
+ metric.update(value);
+ }
+
+ /**
+ * Updates the rate metric with the given name.
+ *
+ * @param metricName
+ * the name of the metric
+ * @param value
+ * the value to add
+ * @param rateInterval
+ * the rate interval [ms]
+ * @param reportingInterval
+ * the reporting interval [ms]
+ */
+ private void updateRateMetric(final String metricName, final int value, final int rateInterval, final int reportingInterval)
+ {
+ Metric metric = metricsRegistry.get(metricName);
+ if (metric == null)
+ {
+ metric = getExistingOrAddNewMetric(metricName, new RateMetric(rateInterval, reportingInterval));
+ }
+
+ metric.update(value);
+ }
+
+ /**
+ * Updates the value metric with the given name.
+ *
+ * @param metricName
+ * the name of the metric
+ * @param value
+ * the value
+ */
+ private void updateValueMetric(final String metricName, final int value)
+ {
+ Metric metric = metricsRegistry.get(metricName);
+ if (metric == null)
+ {
+ metric = getExistingOrAddNewMetric(metricName, new ValueMetric());
+ }
+
+ metric.update(value);
+ }
+
+ /**
+ * Gets a metric from the metrics registry, or if it does not exist yet, adds the given metric to the registry and
+ * returns that one.
+ *
+ * @param metricName
+ * the name of the metric
+ * @param newMetric
+ * the metric to add in case there is no such metric yet
+ */
+ private synchronized Metric getExistingOrAddNewMetric(final String metricName, final Metric newMetric)
+ {
+ Metric metric = metricsRegistry.get(metricName);
+ if (metric == null)
+ {
+ metric = newMetric;
+ metricsRegistry.put(metricName, metric);
+ }
+
+ return metric;
+ }
+
+ /**
+ * Sanitizes the given full metric name such that any invalid character is replaced with an underscore. Valid
+ * characters are letters, numbers, the underscore, the minus sign, and the dot.
+ *
+ * @param metricName
+ * the name of the metric
+ * @return the sanitized metric name
+ */
+ private String sanitizeFullMetricName(final String metricName)
+ {
+ return sanitizeMetricName(metricName, true);
+ }
+
+ /**
+ * Sanitizes the given metric name part such that any invalid character is replaced with an underscore. Valid
+ * characters are letters, numbers, the underscore, and the minus sign.
+ *
+ * @param metricNamePart
+ * one part of a full metric name
+ * @return the sanitized metric name part
+ */
+ private String sanitizeMetricNamePart(final String metricNamePart)
+ {
+ return sanitizeMetricName(metricNamePart, false);
+ }
+
+ /**
+ * Sanitizes the given metric name such that any invalid character is replaced with an underscore. Valid characters
+ * are letters, numbers, the underscore, and the minus sign. Whether or not the dot is valid as well can be
+ * controlled by the second parameter.
+ *
+ * Note that Graphite may allow more characters, but not always in all combinations (e.g., aaa[] is OK, while aaa[a]
+ * is not). That's why we are somewhat over-restrictive here. Better safe than sorry.
+ *
+ * @param metricName
+ * the name of the metric
+ * @param dotsAllowed
+ * whether a dot is also a valid character
+ * @return the sanitized metric name
+ */
+ private String sanitizeMetricName(final String metricName, boolean dotsAllowed)
+ {
+ final char[] chars = metricName.toCharArray();
+
+ for (int i = 0; i < chars.length; i++)
+ {
+ final char c = chars[i];
+ final boolean isValidChar = CharUtils.isAsciiAlphanumeric(c) || c == '_' || c == '-' || (c == '.' && dotsAllowed);
+
+ if (!isValidChar)
+ {
+ chars[i] = '_';
+ }
+ }
+
+ return new String(chars);
+ }
+
+}
diff --git a/src/main/java/com/xceptance/xlt/engine/metrics/otel/AgentResourceProvider.java b/src/main/java/com/xceptance/xlt/engine/metrics/otel/AgentResourceProvider.java
new file mode 100644
index 000000000..76b439038
--- /dev/null
+++ b/src/main/java/com/xceptance/xlt/engine/metrics/otel/AgentResourceProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.xceptance.xlt.engine.metrics.otel;
+
+import com.xceptance.common.util.ProductInformation;
+import com.xceptance.xlt.api.engine.Session;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.semconv.ServiceAttributes;
+
+public class AgentResourceProvider implements ResourceProvider
+{
+ // Service attributes that are still experimental
+
+ private static final AttributeKey SERVICE_NAMESPACE = AttributeKey.stringKey("service.namespace");
+
+ private static final AttributeKey SERVICE_INSTANCE_ID = AttributeKey.stringKey("service.instance.id");
+
+ @Override
+ public Resource createResource(final ConfigProperties config)
+ {
+ final ProductInformation productInfo = ProductInformation.getProductInformation();
+ Attributes atts = Attributes.of(SERVICE_NAMESPACE, "com.xceptance.xlt",
+ ServiceAttributes.SERVICE_NAME, "loadtest-agent",
+ ServiceAttributes.SERVICE_VERSION, productInfo.getVersion(),
+ SERVICE_INSTANCE_ID, Session.getCurrent().getAgentID());
+ return Resource.create(atts);
+ }
+}
diff --git a/src/main/java/com/xceptance/xlt/engine/metrics/otel/OpenTelemetryFactory.java b/src/main/java/com/xceptance/xlt/engine/metrics/otel/OpenTelemetryFactory.java
new file mode 100644
index 000000000..544a89c68
--- /dev/null
+++ b/src/main/java/com/xceptance/xlt/engine/metrics/otel/OpenTelemetryFactory.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.xceptance.xlt.engine.metrics.otel;
+
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.xceptance.xlt.api.util.XltProperties;
+import com.xceptance.xlt.common.XltConstants;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
+
+/**
+ * Factory for creation of pre-configured implementations of OpenTelemetry.
+ */
+public final class OpenTelemetryFactory
+{
+ /** Default c'tor. Declared private to prevent external instantiation. */
+ private OpenTelemetryFactory()
+ {
+ // empty on purpose
+ }
+
+ /**
+ * Creates a new pre-configured OpenTelemetry instance using the properties found in given {@link XltProperties}.
+ *
+ * @param props
+ * the properties to use for configuration
+ * @param propsCustomizer
+ * an optional property customizer
+ * @return ready-to-use OpenTelemetry instance
+ */
+ public static OpenTelemetry create(final XltProperties props, final Consumer