/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.jmeter.visualizers.backend.influxdb; import java.text.DecimalFormat; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.apache.jmeter.config.Arguments; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.util.JMeterUtils; import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient; import org.apache.jmeter.visualizers.backend.BackendListenerContext; import org.apache.jmeter.visualizers.backend.SamplerMetric; import org.apache.jmeter.visualizers.backend.UserMetric; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implementation of {@link AbstractBackendListenerClient} to write in an InfluxDB using * custom schema * @since 3.2 */ public class InfluxdbBackendListenerClient extends AbstractBackendListenerClient implements Runnable { private static final Logger log = LoggerFactory.getLogger(InfluxdbBackendListenerClient.class); private ConcurrentHashMap<String, SamplerMetric> metricsPerSampler = new ConcurrentHashMap<>(); // Name of the measurement private static final String EVENTS_FOR_ANNOTATION = "events"; private static final String TAGS = ",tags="; private static final String TEXT = "text=\""; // Name of the measurement private static final String DEFAULT_MEASUREMENT = "jmeter"; private static final String TAG_TRANSACTION = ",transaction="; private static final String TAG_STATUT = ",statut="; private static final String TAG_APPLICATION = ",application="; private static final String METRIC_COUNT = "count="; private static final String METRIC_COUNT_ERREUR = "countError="; private static final String METRIC_MIN = "min="; private static final String METRIC_MAX = "max="; private static final String METRIC_AVG = "avg="; private static final String METRIC_HIT = "hit="; private static final String METRIC_PCT = "pct"; private static final String METRIC_MAXAT = "maxAT="; private static final String METRIC_MINAT = "minAT="; private static final String METRIC_MEANAT = "meanAT="; private static final String METRIC_STARTEDT = "startedT="; private static final String METRIC_ENDEDT = "endedT="; private static final String TAG_OK = "ok"; private static final String TAG_KO = "ko"; private static final String TAG_ALL = "all"; private static final String CUMULATED_METRICS = "all"; private static final long SEND_INTERVAL = JMeterUtils.getPropDefault("backend_influxdb.send_interval", 5); private static final int MAX_POOL_SIZE = 1; private static final String SEPARATOR = ";"; //$NON-NLS-1$ private static final Object LOCK = new Object(); private boolean summaryOnly; private String measurement = "DEFAULT_MEASUREMENT"; private String influxdbUrl = ""; private String samplersRegex = ""; private Pattern samplersToFilter; private Map<String, Float> okPercentiles; private Map<String, Float> koPercentiles; private Map<String, Float> allPercentiles; private String testTitle; private String testTags; // Name of the application tested private String application = ""; private InfluxdbMetricsSender influxdbMetricsManager; private ScheduledExecutorService scheduler; private ScheduledFuture<?> timerHandle; public InfluxdbBackendListenerClient() { super(); } @Override public void run() { sendMetrics(); } /** * Send metrics */ protected void sendMetrics() { synchronized (LOCK) { for (Map.Entry<String, SamplerMetric> entry : getMetricsInfluxdbPerSampler().entrySet()) { SamplerMetric metric = entry.getValue(); if (entry.getKey().equals(CUMULATED_METRICS)) { addCumulatedMetrics(metric); } else { addMetrics(AbstractInfluxdbMetricsSender.tagToStringValue(entry.getKey()), metric); } // We are computing on interval basis so cleanup metric.resetForTimeInterval(); } } UserMetric userMetrics = getUserMetrics(); // For JMETER context StringBuilder tag = new StringBuilder(60); tag.append(TAG_APPLICATION).append(application); tag.append(TAG_TRANSACTION).append("internal"); StringBuilder field = new StringBuilder(80); field.append(METRIC_MINAT).append(userMetrics.getMinActiveThreads()).append(","); field.append(METRIC_MAXAT).append(userMetrics.getMaxActiveThreads()).append(","); field.append(METRIC_MEANAT).append(userMetrics.getMeanActiveThreads()).append(","); field.append(METRIC_STARTEDT).append(userMetrics.getStartedThreads()).append(","); field.append(METRIC_ENDEDT).append(userMetrics.getFinishedThreads()); influxdbMetricsManager.addMetric(measurement, tag.toString(), field.toString()); influxdbMetricsManager.writeAndSendMetrics(); } /** * Add request metrics to metrics manager. * * @param metric * {@link SamplerMetric} */ private void addMetrics(String transaction, SamplerMetric metric) { // FOR ALL STATUS addMetric(transaction, metric, metric.getTotal(), false, TAG_ALL, metric.getAllMean(), metric.getAllMinTime(), metric.getAllMaxTime(), allPercentiles.values()); // FOR OK STATUS addMetric(transaction, metric, metric.getSuccesses(), false, TAG_OK, metric.getOkMean(), metric.getOkMinTime(), metric.getOkMaxTime(), Collections.<Float> emptySet()); // FOR KO STATUS addMetric(transaction, metric, metric.getFailures(), true, TAG_KO, metric.getKoMean(), metric.getKoMinTime(), metric.getKoMaxTime(), Collections.<Float> emptySet()); } private void addMetric(String transaction, SamplerMetric metric, int count, boolean includeResponseCode, String statut, double mean, double minTime, double maxTime, Collection<Float> pcts) { if (count > 0) { StringBuilder tag = new StringBuilder(70); tag.append(TAG_APPLICATION).append(application); tag.append(TAG_STATUT).append(statut); tag.append(TAG_TRANSACTION).append(transaction); StringBuilder field = new StringBuilder(80); field.append(METRIC_COUNT).append(count); if (!Double.isNaN(mean)) { field.append(",").append(METRIC_AVG).append(mean); } if (!Double.isNaN(minTime)) { field.append(",").append(METRIC_MIN).append(minTime); } if (!Double.isNaN(maxTime)) { field.append(",").append(METRIC_MAX).append(maxTime); } for (Float pct : pcts) { field.append(",").append(METRIC_PCT).append(pct).append("=").append(metric.getAllPercentile(pct)); } influxdbMetricsManager.addMetric(measurement, tag.toString(), field.toString()); } } private void addCumulatedMetrics(SamplerMetric metric) { int total = metric.getTotal(); if (total > 0) { StringBuilder tag = new StringBuilder(70); StringBuilder field = new StringBuilder(100); Collection<Float> pcts = allPercentiles.values(); tag.append(TAG_APPLICATION).append(application); tag.append(TAG_TRANSACTION).append(CUMULATED_METRICS); tag.append(TAG_STATUT).append(CUMULATED_METRICS); field.append(METRIC_COUNT).append(total); field.append(",").append(METRIC_COUNT_ERREUR).append(metric.getFailures()); if (!Double.isNaN(metric.getOkMean())) { field.append(",").append(METRIC_AVG).append(Double.toString(metric.getOkMean())); } if (!Double.isNaN(metric.getOkMinTime())) { field.append(",").append(METRIC_MIN).append(Double.toString(metric.getOkMinTime())); } if (!Double.isNaN(metric.getOkMaxTime())) { field.append(",").append(METRIC_MAX).append(Double.toString(metric.getOkMaxTime())); } field.append(",").append(METRIC_HIT).append(metric.getHits()); for (Float pct : pcts) { field.append(",").append(METRIC_PCT).append(pct).append("=").append(Double.toString(metric.getAllPercentile(pct))); } field.append(",").append(METRIC_HIT).append(metric.getHits()); influxdbMetricsManager.addMetric(measurement, tag.toString(), field.toString()); } } /** * @return the samplersList */ public String getSamplersRegex() { return samplersRegex; } /** * @param samplersList * the samplersList to set */ public void setSamplersList(String samplersList) { this.samplersRegex = samplersList; } @Override public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) { synchronized (LOCK) { UserMetric userMetrics = getUserMetrics(); for (SampleResult sampleResult : sampleResults) { userMetrics.add(sampleResult); Matcher matcher = samplersToFilter.matcher(sampleResult.getSampleLabel()); if (!summaryOnly && (matcher.find())) { SamplerMetric samplerMetric = getSamplerMetricInfluxdb(sampleResult.getSampleLabel()); samplerMetric.add(sampleResult); } SamplerMetric cumulatedMetrics = getSamplerMetricInfluxdb(CUMULATED_METRICS); cumulatedMetrics.add(sampleResult); } } } @Override public void setupTest(BackendListenerContext context) throws Exception { String influxdbMetricsSender = context.getParameter("influxdbMetricsSender"); influxdbUrl = context.getParameter("influxdbUrl"); summaryOnly = context.getBooleanParameter("summaryOnly", false); samplersRegex = context.getParameter("samplersRegex", ""); application = AbstractInfluxdbMetricsSender.tagToStringValue(context.getParameter("application", "")); measurement = AbstractInfluxdbMetricsSender .tagToStringValue(context.getParameter("measurement", DEFAULT_MEASUREMENT)); testTitle = context.getParameter("testTitle", "Test"); testTags = AbstractInfluxdbMetricsSender.tagToStringValue(context.getParameter("eventTags", "")); String percentilesAsString = context.getParameter("percentiles", ""); String[] percentilesStringArray = percentilesAsString.split(SEPARATOR); okPercentiles = new HashMap<>(percentilesStringArray.length); koPercentiles = new HashMap<>(percentilesStringArray.length); allPercentiles = new HashMap<>(percentilesStringArray.length); DecimalFormat format = new DecimalFormat("0.##"); for (int i = 0; i < percentilesStringArray.length; i++) { if (!StringUtils.isEmpty(percentilesStringArray[i].trim())) { try { Float percentileValue = Float.valueOf(percentilesStringArray[i].trim()); okPercentiles.put(AbstractInfluxdbMetricsSender.tagToStringValue(format.format(percentileValue)), percentileValue); koPercentiles.put(AbstractInfluxdbMetricsSender.tagToStringValue(format.format(percentileValue)), percentileValue); allPercentiles.put(AbstractInfluxdbMetricsSender.tagToStringValue(format.format(percentileValue)), percentileValue); } catch (Exception e) { log.error("Error parsing percentile: '{}'", percentilesStringArray[i], e); } } } Class<?> clazz = Class.forName(influxdbMetricsSender); this.influxdbMetricsManager = (InfluxdbMetricsSender) clazz.newInstance(); influxdbMetricsManager.setup(influxdbUrl); samplersToFilter = Pattern.compile(samplersRegex); addAnnotation(true); scheduler = Executors.newScheduledThreadPool(MAX_POOL_SIZE); // Start immediately the scheduler and put the pooling ( 5 seconds by default ) this.timerHandle = scheduler.scheduleAtFixedRate(this, 0, SEND_INTERVAL, TimeUnit.SECONDS); } protected SamplerMetric getSamplerMetricInfluxdb(String sampleLabel) { SamplerMetric samplerMetric = metricsPerSampler.get(sampleLabel); if (samplerMetric == null) { samplerMetric = new SamplerMetric(); SamplerMetric oldValue = metricsPerSampler.putIfAbsent(sampleLabel, samplerMetric); if (oldValue != null) { samplerMetric = oldValue; } } return samplerMetric; } private Map<String, SamplerMetric> getMetricsInfluxdbPerSampler() { return metricsPerSampler; } @Override public void teardownTest(BackendListenerContext context) throws Exception { boolean cancelState = timerHandle.cancel(false); log.debug("Canceled state: {}", cancelState); scheduler.shutdown(); try { scheduler.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException e) { log.error("Error waiting for end of scheduler"); Thread.currentThread().interrupt(); } addAnnotation(false); // Send last set of data before ending log.info("Sending last metrics"); sendMetrics(); influxdbMetricsManager.destroy(); super.teardownTest(context); } /** * Add Annotation at start or end of the run ( usefull with Grafana ) * Grafana will let you send HTML in the "Text" such as a link to the release notes * Tags are separated by spaces in grafana * Tags is put as InfluxdbTag for better query performance on it * Never double or single quotes in influxdb except for string field * see : https://docs.influxdata.com/influxdb/v1.1/write_protocols/line_protocol_reference/#quoting-special-characters-and-additional-naming-guidelines * * @param startOrEnd boolean true for start, false for end */ private void addAnnotation(boolean startOrEnd) { influxdbMetricsManager.addMetric(EVENTS_FOR_ANNOTATION, TAG_APPLICATION + application + ",title=ApacheJMeter"+ (StringUtils.isNotEmpty(testTags) ? TAGS+ testTags : ""), TEXT + AbstractInfluxdbMetricsSender.fieldToStringValue(testTitle + (startOrEnd ? " started" : " ended")) + "\"" ); } @Override public Arguments getDefaultParameters() { Arguments arguments = new Arguments(); arguments.addArgument("influxdbMetricsSender", HttpMetricsSender.class.getName()); arguments.addArgument("influxdbUrl", ""); arguments.addArgument("application", "application name"); arguments.addArgument("measurement", DEFAULT_MEASUREMENT); arguments.addArgument("summaryOnly", "false"); arguments.addArgument("samplersRegex", ".*"); arguments.addArgument("percentiles", "99,95,90"); arguments.addArgument("testTitle", "Test name"); arguments.addArgument("eventTags", ""); return arguments; } }