package nl.codecentric.jenkins.appd; import hudson.Extension; import hudson.Launcher; import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import nl.codecentric.jenkins.appd.rest.RestConnection; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import static nl.codecentric.jenkins.appd.util.LocalMessages.PUBLISHER_DISPLAYNAME; /** * Main class for this Jenkins Plugin.<br /> * Hooks into the build flow as post-build step, then collecting data and generating the report.<br /><br /> * <p/> * Configuration is set from the Jenkins Build Configuration menu. When a build is triggered, the * {@link AppDynamicsResultsPublisher#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.BuildListener)} * method is called. This will then trigger the {@link AppDynamicsDataCollector} and parse any results and produces * {@link AppDynamicsReport}'s.<br /> * A {@link AppDynamicsBuildAction} is used to store data per-build, so it can be compared later. */ public class AppDynamicsResultsPublisher extends Recorder { private static final String DEFAULT_USERNAME = "username@customer1"; private static final String DEFAULT_THRESHOLD_METRIC = "Overall Application Performance|Average Response Time (ms)"; private static final int DEFAULT_THRESHOLD_UNSTABLE = 80; private static final int DEFAULT_THRESHOLD_FAILED = 65; private static final int DEFAULT_MINIMUM_MEASURE_TIME_MINUTES = 10; public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { @Override public String getDisplayName() { return PUBLISHER_DISPLAYNAME.toString(); } @Override public String getHelpFile() { return "/plugin/appdynamics-dashboard/help.html"; } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } public String getDefaultUsername() { return DEFAULT_USERNAME; } public int getDefaultMinimumMeasureTimeInMinutes() { return DEFAULT_MINIMUM_MEASURE_TIME_MINUTES; } public int getDefaultUnstableThreshold() { return DEFAULT_THRESHOLD_UNSTABLE; } public int getDefaultFailedThreshold() { return DEFAULT_THRESHOLD_FAILED; } public ListBoxModel doFillThresholdMetricItems() { ListBoxModel model = new ListBoxModel(); for (String value : AppDynamicsDataCollector.getAvailableMetricPaths()) { model.add(value); } return model; } public FormValidation doCheckAppdynamicsRestUri(@QueryParameter final String appdynamicsRestUri) { FormValidation validationResult; if (RestConnection.validateRestUri(appdynamicsRestUri)) { validationResult = FormValidation.ok(); } else { validationResult = FormValidation.error("AppDynamics REST uri is not valid, cannot be empty and has to " + "start with 'http://' or 'https://'"); } return validationResult; } public FormValidation doCheckUsername(@QueryParameter final String username) { FormValidation validationResult; if (RestConnection.validateUsername(username)) { validationResult = FormValidation.ok(); } else { validationResult = FormValidation.error("Username for REST interface cannot be empty"); } return validationResult; } public FormValidation doCheckPassword(@QueryParameter final String password) { FormValidation validationResult; if (RestConnection.validatePassword(password)) { validationResult = FormValidation.ok(); } else { validationResult = FormValidation.error("Password for REST interface cannot be empty"); } return validationResult; } public FormValidation doCheckApplicationName(@QueryParameter final String applicationName) { FormValidation validationResult; if (RestConnection.validateApplicationName(applicationName)) { validationResult = FormValidation.ok(); } else { validationResult = FormValidation.error("AppDynamics application name cannot be empty"); } return validationResult; } public FormValidation doTestAppDynamicsConnection(@QueryParameter("appdynamicsRestUri") final String appdynamicsRestUri, @QueryParameter("username") final String username, @QueryParameter("password") final String password, @QueryParameter("applicationName") final String applicationName) { FormValidation validationResult; RestConnection connection = new RestConnection(appdynamicsRestUri, username, password, applicationName); if (connection.validateConnection()) { validationResult = FormValidation.ok("Connection successful"); } else { validationResult = FormValidation.warning("Connection with AppDynamics RESTful interface could not be established"); } return validationResult; } } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); private RestConnection connection; /** * Below fields are configured via the <code>config.jelly</code> page. */ private String appdynamicsRestUri = ""; private String username = ""; private String password = ""; private String applicationName = ""; private String thresholdMetric = DEFAULT_THRESHOLD_METRIC; private Boolean lowerIsBetter = true; private Integer minimumMeasureTimeInMinutes = DEFAULT_MINIMUM_MEASURE_TIME_MINUTES; private Integer performanceFailedThreshold = DEFAULT_THRESHOLD_FAILED; private Integer performanceUnstableThreshold = DEFAULT_THRESHOLD_UNSTABLE; @DataBoundConstructor public AppDynamicsResultsPublisher(final String appdynamicsRestUri, final String username, final String password, final String applicationName, final String thresholdMetric, final Boolean lowerIsBetter, final Integer minimumMeasureTimeInMinutes, final Integer performanceFailedThreshold, final Integer performanceUnstableThreshold) { setAppdynamicsRestUri(appdynamicsRestUri); setUsername(username); setPassword(password); setApplicationName(applicationName); setThresholdMetric(thresholdMetric); setLowerIsBetter(lowerIsBetter); setMinimumMeasureTimeInMinutes(minimumMeasureTimeInMinutes); setPerformanceFailedThreshold(performanceFailedThreshold); setPerformanceUnstableThreshold(performanceUnstableThreshold); } @Override public BuildStepDescriptor<Publisher> getDescriptor() { return DESCRIPTOR; } @Override public Action getProjectAction(AbstractProject<?, ?> project) { return new AppDynamicsProjectAction(project, thresholdMetric, AppDynamicsDataCollector.getAvailableMetricPaths()); } public BuildStepMonitor getRequiredMonitorService() { // No synchronization necessary between builds return BuildStepMonitor.NONE; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { PrintStream logger = listener.getLogger(); RestConnection connection = new RestConnection(appdynamicsRestUri, username, password, applicationName); logger.println("Verify connection to AppDynamics REST interface ..."); if (!connection.validateConnection()) { logger.println("Connection to AppDynamics REST interface unsuccessful, cannot proceed with this build step"); if (build.getResult().isBetterOrEqualTo(Result.UNSTABLE)) build.setResult(Result.FAILURE); return true; } logger.println("Connection successful, continue to fetch measurements from AppDynamics Controller ..."); AppDynamicsDataCollector dataCollector = new AppDynamicsDataCollector(connection, build, minimumMeasureTimeInMinutes); AppDynamicsReport report = dataCollector.createReportFromMeasurements(); AppDynamicsBuildAction buildAction = new AppDynamicsBuildAction(build, report); build.addAction(buildAction); logger.println("Ready building AppDynamics report"); logger.println("Verifying for improving or degrading performance, main metric: " + thresholdMetric + " where lower is better = " + lowerIsBetter); try { // Verify if the necessary metric is successfully fetched. report.getMetricByKey(this.thresholdMetric); } catch (Exception e) { logger.println("Unable to fetch (threshold) metric to determine if build is degrading. Aborting"); if (build.getResult().isBetterOrEqualTo(Result.UNSTABLE)) build.setResult(Result.FAILURE); return true; } if (performanceUnstableThreshold >= 0 && performanceUnstableThreshold <= 100) { logger.println("Performance degradation greater or equal than " + performanceUnstableThreshold + "% sets the build as " + Result.UNSTABLE.toString().toLowerCase()); } else { logger.println("Performance: No threshold configured for making the test " + Result.UNSTABLE.toString().toLowerCase()); } if (performanceFailedThreshold >= 0 && performanceFailedThreshold <= 100) { logger.println("Performance degradation greater or equal than " + performanceFailedThreshold + "% sets the build as " + Result.FAILURE.toString().toLowerCase()); } else { logger.println("Performance: No threshold configured for making the test " + Result.FAILURE.toString().toLowerCase()); } // mark the build as unstable or failure depending on the outcome. List<AppDynamicsReport> previousReportList = getListOfPreviousReports(build, report.getTimestamp()); logger.println("Number of old reports located for average: " + previousReportList.size()); double averageOverTime = calculateAverageBasedOnPreviousReports(previousReportList); logger.println("Calculated average from previous reports: " + averageOverTime); double currentReportAverage = report.getAverageForMetric(thresholdMetric); logger.println("Current report average: " + currentReportAverage); double performanceAsPercentageOfAverage; if (lowerIsBetter) { performanceAsPercentageOfAverage = (averageOverTime / currentReportAverage) * 100; } else { performanceAsPercentageOfAverage = (currentReportAverage / averageOverTime) * 100; } logger.println("Current average as percentage of total average: " + performanceAsPercentageOfAverage + "%"); Result result; if (performanceFailedThreshold >= 0 && performanceAsPercentageOfAverage - performanceFailedThreshold < 0) { build.setResult(Result.FAILURE); } else if (performanceUnstableThreshold >= 0 && performanceAsPercentageOfAverage - performanceUnstableThreshold < 0) { result = Result.UNSTABLE; if (result.isWorseThan(build.getResult())) { build.setResult(result); } } logger.println("Metric: " + thresholdMetric + " reported performance compared to average of " + performanceAsPercentageOfAverage + "% . Build status is: " + build.getResult()); return true; } private double calculateAverageBasedOnPreviousReports(final List<AppDynamicsReport> reports) { double calculatedSum = 0; int numberOfMeasurements = 0; for (AppDynamicsReport report : reports) { double value = report.getAverageForMetric(thresholdMetric); if (value >= 0) { calculatedSum += value; numberOfMeasurements++; } } double result = -1; if (numberOfMeasurements > 0) { result = calculatedSum / numberOfMeasurements; } return result; } private List<AppDynamicsReport> getListOfPreviousReports(final AbstractBuild<?, ?> build, final long currentTimestamp) { final List<AppDynamicsReport> previousReports = new ArrayList<AppDynamicsReport>(); final List<? extends AbstractBuild<?, ?>> builds = build.getProject().getBuilds(); for (AbstractBuild<?, ?> currentBuild : builds) { final AppDynamicsBuildAction performanceBuildAction = currentBuild.getAction(AppDynamicsBuildAction.class); if (performanceBuildAction == null) { continue; } final AppDynamicsReport report = performanceBuildAction.getBuildActionResultsDisplay().getAppDynamicsReport(); if (report != null && (report.getTimestamp() != currentTimestamp || builds.size() == 1)) { previousReports.add(report); } } return previousReports; } public String getAppdynamicsRestUri() { return appdynamicsRestUri; } public void setAppdynamicsRestUri(final String appdynamicsRestUri) { this.appdynamicsRestUri = appdynamicsRestUri; } public String getUsername() { return username; } public void setUsername(final String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(final String password) { this.password = password; } public String getApplicationName() { return applicationName; } public void setApplicationName(final String applicationName) { this.applicationName = applicationName; } public String getThresholdMetric() { return thresholdMetric; } public void setThresholdMetric(String thresholdMetric) { if (thresholdMetric == null || thresholdMetric.isEmpty()) { this.thresholdMetric = DEFAULT_THRESHOLD_METRIC; } else { this.thresholdMetric = thresholdMetric; } } public Boolean getLowerIsBetter() { return lowerIsBetter; } public void setLowerIsBetter(Boolean lowerIsBetter) { this.lowerIsBetter = lowerIsBetter; } public Integer getMinimumMeasureTimeInMinutes() { return minimumMeasureTimeInMinutes; } public void setMinimumMeasureTimeInMinutes(final Integer minimumMeasureTimeInMinutes) { this.minimumMeasureTimeInMinutes = Math.max(DEFAULT_MINIMUM_MEASURE_TIME_MINUTES, Math.min(minimumMeasureTimeInMinutes, 1440)); } public Integer getPerformanceFailedThreshold() { return performanceFailedThreshold; } public void setPerformanceFailedThreshold(final Integer performanceFailedThreshold) { this.performanceFailedThreshold = Math.max(0, Math.min(performanceFailedThreshold, 100)); } public Integer getPerformanceUnstableThreshold() { return performanceUnstableThreshold; } public void setPerformanceUnstableThreshold(final Integer performanceUnstableThreshold) { this.performanceUnstableThreshold = Math.max(0, Math.min(performanceUnstableThreshold, 100)); } }