From 885859972e0360719ac09cc31b4de9931e859aaa Mon Sep 17 00:00:00 2001 From: Alberto Hormazabal Date: Mon, 3 Dec 2018 16:25:22 -0300 Subject: [PATCH 1/4] * updates default api version to 25 * new 'metrics' command and subcommands: rd metrics data - Prints the metrics data rd metrics healthcheck - Print health check status information rd metrics list - Print system information and stats rd metrics ping - Returns a simple response rd metrics threads - Print system threads status information --- .../org/rundeck/client/RundeckClient.java | 7 +- .../org/rundeck/client/api/RundeckApi.java | 39 ++ .../api/model/metrics/EndpointListResult.java | 52 +++ .../api/model/metrics/HealthCheckStatus.java | 53 +++ .../client/api/model/metrics/MetricsData.java | 83 ++++ .../java/org/rundeck/client/tool/Main.java | 18 +- .../rundeck/client/tool/commands/Metrics.java | 291 ++++++++++++++ .../client/tool/commands/MetricsSpec.groovy | 378 ++++++++++++++++++ .../client/tool/commands/SchedulerSpec.groovy | 10 +- 9 files changed, 913 insertions(+), 18 deletions(-) create mode 100644 rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/EndpointListResult.java create mode 100644 rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/HealthCheckStatus.java create mode 100644 rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/MetricsData.java create mode 100644 src/main/java/org/rundeck/client/tool/commands/Metrics.java create mode 100644 src/test/groovy/org/rundeck/client/tool/commands/MetricsSpec.groovy diff --git a/rd-api-client/src/main/java/org/rundeck/client/RundeckClient.java b/rd-api-client/src/main/java/org/rundeck/client/RundeckClient.java index 51c3f20b..ea37d444 100644 --- a/rd-api-client/src/main/java/org/rundeck/client/RundeckClient.java +++ b/rd-api-client/src/main/java/org/rundeck/client/RundeckClient.java @@ -28,7 +28,10 @@ import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -40,7 +43,7 @@ */ public class RundeckClient { public static final String USER_AGENT = Version.NAME + "/" + Version.VERSION; - public static final int API_VERS = 21; + public static final int API_VERS = 25; public static final Pattern API_VERS_PATTERN = Pattern.compile("^(.*)(/api/(\\d+)/?)$"); public static final String ENV_BYPASS_URL = "RD_BYPASS_URL"; public static final String ENV_INSECURE_SSL = "RD_INSECURE_SSL"; diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java index 5657deab..c6727aea 100644 --- a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java +++ b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java @@ -20,6 +20,9 @@ import okhttp3.RequestBody; import okhttp3.ResponseBody; import org.rundeck.client.api.model.*; +import org.rundeck.client.api.model.metrics.EndpointListResult; +import org.rundeck.client.api.model.metrics.HealthCheckStatus; +import org.rundeck.client.api.model.metrics.MetricsData; import org.rundeck.client.api.model.repository.ArtifactActionMessage; import org.rundeck.client.api.model.repository.RepositoryArtifacts; import org.rundeck.client.api.model.scheduler.ScheduledJobItem; @@ -1153,4 +1156,40 @@ Call bulkDisableJobSchedule( ); + // Metrics calls + /** + * @see API + */ + @Headers("Accept: application/json") + @GET("metrics") + Call listMetricsEndpoints(); + + /** + * @see API + */ + @Headers("Accept: application/json") + @GET("metrics/healthcheck") + Call> getHealthCheckMetrics(); + + /** + * @see API + */ + @Headers("Accept: application/json") + @GET("metrics/threads") + Call getThreadMetrics(); + + /** + * @see API + */ + @Headers("Accept: application/json") + @GET("metrics/ping") + Call getPing(); + + /** + * @see API + */ + @Headers("Accept: application/json") + @GET("metrics/metrics") + Call getMetricsData(); + } diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/EndpointListResult.java b/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/EndpointListResult.java new file mode 100644 index 00000000..367ba72b --- /dev/null +++ b/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/EndpointListResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Rundeck, Inc. (http://rundeck.com) + * + * 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 org.rundeck.client.api.model.metrics; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.rundeck.client.api.model.sysinfo.Link; + +import java.util.Map; + + +@JsonIgnoreProperties(ignoreUnknown = true) +public class EndpointListResult { + + private Map endpointLinks; + + public int size() { + return endpointLinks != null ? endpointLinks.size() : 0; + } + + @JsonProperty("_links") + public Map getEndpointLinks() { + return endpointLinks; + } + + @JsonProperty("_links") + public EndpointListResult setEndpointLinks(Map endpointLinks) { + this.endpointLinks = endpointLinks; + return this; + } + + @Override + public String toString() { + return "EndpointListResult{" + + "endpointLinks=" + endpointLinks + + '}'; + } +} diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/HealthCheckStatus.java b/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/HealthCheckStatus.java new file mode 100644 index 00000000..a7187d9e --- /dev/null +++ b/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/HealthCheckStatus.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Rundeck, Inc. (http://rundeck.com) + * + * 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 org.rundeck.client.api.model.metrics; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + + +@JsonIgnoreProperties(ignoreUnknown = true) +public class HealthCheckStatus { + + private boolean healthy; + private String message; + + public boolean isHealthy() { + return healthy; + } + + public HealthCheckStatus setHealthy(boolean healthy) { + this.healthy = healthy; + return this; + } + + public String getMessage() { + return message; + } + + public HealthCheckStatus setMessage(String message) { + this.message = message; + return this; + } + + @Override + public String toString() { + return "HealthCheckStatus{" + + "healthy=" + healthy + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/MetricsData.java b/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/MetricsData.java new file mode 100644 index 00000000..20333d64 --- /dev/null +++ b/rd-api-client/src/main/java/org/rundeck/client/api/model/metrics/MetricsData.java @@ -0,0 +1,83 @@ +package org.rundeck.client.api.model.metrics; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class MetricsData { + + private String version; + private Map> gauges; + private Map> counters; + private Map> histograms; + private Map> meters; + private Map> timers; + + + public String getVersion() { + return version; + } + + public MetricsData setVersion(String version) { + this.version = version; + return this; + } + + public Map> getGauges() { + return gauges; + } + + public MetricsData setGauges(Map> gauges) { + this.gauges = gauges; + return this; + } + + public Map> getCounters() { + return counters; + } + + public MetricsData setCounters(Map> counters) { + this.counters = counters; + return this; + } + + public Map> getHistograms() { + return histograms; + } + + public MetricsData setHistograms(Map> histograms) { + this.histograms = histograms; + return this; + } + + public Map> getMeters() { + return meters; + } + + public MetricsData setMeters(Map> meters) { + this.meters = meters; + return this; + } + + public Map> getTimers() { + return timers; + } + + public MetricsData setTimers(Map> timers) { + this.timers = timers; + return this; + } + + @Override + public String toString() { + return "MetricsData{" + + "version='" + version + '\'' + + ", gauges=" + gauges + + ", counters=" + counters + + ", histograms=" + histograms + + ", meters=" + meters + + ", timers=" + timers + + '}'; + } +} diff --git a/src/main/java/org/rundeck/client/tool/Main.java b/src/main/java/org/rundeck/client/tool/Main.java index 5bb4dd0f..6106ce13 100644 --- a/src/main/java/org/rundeck/client/tool/Main.java +++ b/src/main/java/org/rundeck/client/tool/Main.java @@ -16,18 +16,20 @@ package org.rundeck.client.tool; +import org.rundeck.client.RundeckClient; +import org.rundeck.client.api.RequestFailed; +import org.rundeck.client.api.RundeckApi; +import org.rundeck.client.api.model.DateInfo; +import org.rundeck.client.api.model.Execution; +import org.rundeck.client.api.model.JobItem; import org.rundeck.client.api.model.scheduler.ScheduledJobItem; +import org.rundeck.client.tool.commands.*; import org.rundeck.client.tool.commands.repository.Plugins; +import org.rundeck.client.util.*; import org.rundeck.toolbelt.*; import org.rundeck.toolbelt.format.json.jackson.JsonFormatter; import org.rundeck.toolbelt.format.yaml.snakeyaml.YamlFormatter; import org.rundeck.toolbelt.input.jewelcli.JewelInput; -import org.rundeck.client.RundeckClient; -import org.rundeck.client.api.RequestFailed; -import org.rundeck.client.api.RundeckApi; -import org.rundeck.client.api.model.*; -import org.rundeck.client.tool.commands.*; -import org.rundeck.client.util.*; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.nodes.Tag; @@ -176,8 +178,8 @@ public static Tool tool(final Rd rd) { new Nodes(rd), new Users(rd), new Something(), - new Retry(rd) - + new Retry(rd), + new Metrics(rd) ) .bannerResource("rd-banner.txt") .commandInput(new JewelInput()); diff --git a/src/main/java/org/rundeck/client/tool/commands/Metrics.java b/src/main/java/org/rundeck/client/tool/commands/Metrics.java new file mode 100644 index 00000000..72f13554 --- /dev/null +++ b/src/main/java/org/rundeck/client/tool/commands/Metrics.java @@ -0,0 +1,291 @@ +/* + * Copyright 2017 Rundeck, Inc. (http://rundeck.com) + * + * 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 org.rundeck.client.tool.commands; + +import com.lexicalscope.jewel.cli.CommandLineInterface; +import com.lexicalscope.jewel.cli.Option; +import okhttp3.ResponseBody; +import org.rundeck.client.api.RundeckApi; +import org.rundeck.client.api.model.metrics.EndpointListResult; +import org.rundeck.client.api.model.metrics.HealthCheckStatus; +import org.rundeck.client.api.model.metrics.MetricsData; +import org.rundeck.client.tool.RdApp; +import org.rundeck.client.tool.options.VerboseOption; +import org.rundeck.toolbelt.Command; +import org.rundeck.toolbelt.CommandOutput; +import org.rundeck.toolbelt.InputError; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * scheduler subcommands + */ +@Command(description = "View metrics endpoints information.") +public class Metrics extends AppCommand { + public Metrics(final RdApp client) { + super(client); + } + + + // rd metrics list + + @CommandLineInterface(application = "list") + interface EndpointListOptions extends VerboseOption { + } + + @Command(description = "Print system information and stats.") + public void list(EndpointListOptions options, CommandOutput output) throws IOException, InputError { + + EndpointListResult endpointList = apiCall(RundeckApi::listMetricsEndpoints); + + if (endpointList.size() > 0) { + output.info(endpointList.size() + " metric endpoints:"); + output.output(endpointList.getEndpointLinks().entrySet().stream() + .map(metricEntry -> metricEntry.getKey() + + (options.isVerbose() ? (" " + metricEntry.getValue().getHref()) : "")) + .collect(Collectors.joining("\n", "", "\n")) + ); + } + else { + output.warning("No metrics endpoints found."); + } + } + + + // rd metrics healthcheck + + @CommandLineInterface(application = "healthcheck") + interface HealthCheckOptions { + @Option(shortName = "u", + longName = "unhealthy", + description = "Show only checks with unhealthy status.") + boolean isUnhealthyOnly(); + + } + + @Command(description = "Print health check status information.") + public void healthcheck(HealthCheckOptions options, CommandOutput output) throws IOException, InputError { + + Map healthCheckStatus = apiCall(RundeckApi::getHealthCheckMetrics); + Map statusList; + + if (options.isUnhealthyOnly()) { + statusList = healthCheckStatus.entrySet().stream() + .filter(entry -> !entry.getValue().isHealthy()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue)); + } + else { + statusList = healthCheckStatus; + } + + + if (statusList.size() > 0) { + output.info("Showing current health status for " + statusList.size() + " checks:"); + + output.output(statusList.entrySet().stream() + .map(healthCheckEntry -> String.format("%-11s %s%s", + (healthCheckEntry.getValue().isHealthy() ? "HEALTHY" : "NOT-HEALTHY"), + healthCheckEntry.getKey(), + (healthCheckEntry.getValue().getMessage() != null ? " -- " + healthCheckEntry.getValue().getMessage() : "") + )) + .collect(Collectors.joining( + "\n", + "", + "" + )) + ); + } + else { + output.warning("No results found."); + } + } + + // rd metrics threads + + @CommandLineInterface(application = "threads") + interface ThreadsOptions extends VerboseOption { +// @Option(shortName = "u", +// longName = "unhealthy", +// description = "Show only checks with unhealthy status.") +// boolean isUnhealthyOnly(); + + } + + @Command(description = "Print system threads status information.") + public void threads(ThreadsOptions options, CommandOutput output) throws IOException, InputError { + + ResponseBody response = apiCall(RundeckApi::getThreadMetrics); + + output.info("System threads status: "); + if (options.isVerbose()) { + output.output(response.string()); + } + else { + output.output(Stream.of(response.string().split("\n")) + .filter(s -> s.matches("^\\S+.*")) + .collect(Collectors.joining("\n"))); + } + } + + // rd metrics ping + +// @CommandLineInterface(application = "ping") +// interface PingOptions { +// +// } + + @Command(description = "Returns a simple response.") + public void ping(CommandOutput output) throws IOException, InputError { + + output.info(printTimestamp() + "Pinging server..."); + ResponseBody response = apiCall(RundeckApi::getPing); + output.info(printTimestamp() + response.string()); + } + + /** + * @return A formatted string with the current local time. Such as "[2007-12-03T10:15:30] ". + */ + private String printTimestamp() { + return "[" + LocalDateTime.now().toString() + "] "; + } + + + // rd metrics data + + @CommandLineInterface(application = "threads") + interface MetricsDataOptions { + + @Option(shortName = "s", + longName = "summary", + description = "Show only a summary of metric data selected." + ) + boolean isSummary(); + + @Option(shortName = "a", + longName = "all", + description = "Show all metrics available, which is the default. This option supersedes all other selection options." + ) + boolean isAll(); + + @Option(shortName = "g", + longName = "gauges", + description = "Show all gauge metrics available.") + boolean isGauge(); + + @Option(shortName = "c", + longName = "counters", + description = "Show all counter metrics available.") + boolean isCounter(); + + @Option(shortName = "h", + longName = "histograms", + description = "Show all histogram metrics available.") + boolean isHistograms(); + + @Option(shortName = "m", + longName = "meters", + description = "Show all meter metrics available.") + boolean isMeters(); + + @Option(shortName = "t", + longName = "timers", + description = "Show all timer metrics available.") + boolean isTimers(); + + } + + + @Command(description = "Prints the metrics data.") + public void data(MetricsDataOptions options, CommandOutput output) throws IOException, InputError { + + MetricsData metricsData = apiCall(RundeckApi::getMetricsData); + + output.info("Displaying system metric data:"); + output.info("Version: " + metricsData.getVersion()); + + // Print all if --all is used, or if no selectors are specified. + boolean printAll = options.isAll() || ( + !options.isGauge() && + !options.isCounter() && + !options.isHistograms() && + !options.isMeters() && + !options.isTimers() + ); + + if (printAll || options.isGauge()) { + output.info("Found " + metricsData.getGauges().size() + " gauge metrics."); + if (!options.isSummary()) { + output.output(printMetricMap(metricsData.getGauges())); + } + } + + if (printAll || options.isCounter()) { + output.info("Found " + metricsData.getCounters().size() + " counter metrics."); + if (!options.isSummary()) { + output.output(printMetricMap(metricsData.getCounters())); + } + } + + if (printAll || options.isHistograms()) { + output.info("Found " + metricsData.getHistograms().size() + " histogram metrics."); + if (!options.isSummary()) { + output.output(printMetricMap(metricsData.getHistograms())); + } + } + + if (printAll || options.isMeters()) { + output.info("Found " + metricsData.getMeters().size() + " meter metrics."); + if (!options.isSummary()) { + output.output(printMetricMap(metricsData.getMeters())); + } + } + + if (printAll || options.isTimers()) { + output.info("Found " + metricsData.getTimers().size() + " timer metrics."); + if (!options.isSummary()) { + output.output(printMetricMap(metricsData.getTimers())); + } + } + + } + + + /** + * Generates a printable version of a metric map. + */ + private static String printMetricMap(Map> metricMap) { + return metricMap.entrySet().stream() + .map(metricEntry -> metricEntry.getValue().entrySet().stream() + .map(metricValues -> String.format("- %s: %s", + metricValues.getKey(), + metricValues.getValue() + )) + .collect(Collectors.joining( + "\n", + metricEntry.getKey() + ":\n", + "\n" + ))) + .collect(Collectors.joining("\n")); + } + +} diff --git a/src/test/groovy/org/rundeck/client/tool/commands/MetricsSpec.groovy b/src/test/groovy/org/rundeck/client/tool/commands/MetricsSpec.groovy new file mode 100644 index 00000000..af13b1e7 --- /dev/null +++ b/src/test/groovy/org/rundeck/client/tool/commands/MetricsSpec.groovy @@ -0,0 +1,378 @@ +/* + * Copyright 2017 Rundeck, Inc. (http://rundeck.com) + * + * 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 org.rundeck.client.tool.commands + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.rundeck.client.api.RundeckApi +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import spock.lang.Specification + +/** + * @author ahormazabal + * @since 2018/11/26 + */ +class MetricsSpec extends Specification { + + def "parse metrics data deserialization"() { + given: + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody('''{ + "version": "3.1.3", + "gauges": { + "dataSource.connection.pingTime": { + "value": 1 + }, + "rundeck.scheduler.quartz.runningExecutions": { + "value": 0 + }, + "rundeck.services.AuthorizationService.sourceCache.evictionCount": { + "value": 0 + }, + "rundeck.services.AuthorizationService.sourceCache.hitCount": { + "value": 9 + }, + "rundeck.services.AuthorizationService.sourceCache.hitRate": { + "value": 0.9 + }, + "rundeck.services.AuthorizationService.sourceCache.loadExceptionCount": { + "value": 0 + }, + "rundeck.services.AuthorizationService.sourceCache.missCount": { + "value": 1 + }, + "rundeck.services.NodeService.nodeCache.evictionCount": { + "value": 0 + }, + "rundeck.services.NodeService.nodeCache.hitCount": { + "value": 11 + }, + "rundeck.services.NodeService.nodeCache.hitRate": { + "value": 0.9166666666666666 + }, + "rundeck.services.NodeService.nodeCache.loadExceptionCount": { + "value": 0 + }, + "rundeck.services.NodeService.nodeCache.missCount": { + "value": 1 + }, + "rundeck.services.ProjectManagerService.fileCache.evictionCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.fileCache.hitCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.fileCache.hitRate": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.fileCache.loadExceptionCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.fileCache.missCount": { + "value": 6 + }, + "rundeck.services.ProjectManagerService.projectCache.evictionCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.projectCache.hitCount": { + "value": 530 + }, + "rundeck.services.ProjectManagerService.projectCache.hitRate": { + "value": 0.9888059701492538 + }, + "rundeck.services.ProjectManagerService.projectCache.loadExceptionCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.projectCache.missCount": { + "value": 6 + }, + "rundeck.services.ProjectManagerService.sourceCache.evictionCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.sourceCache.hitCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.sourceCache.hitRate": { + "value": 1 + }, + "rundeck.services.ProjectManagerService.sourceCache.loadExceptionCount": { + "value": 0 + }, + "rundeck.services.ProjectManagerService.sourceCache.missCount": { + "value": 0 + } + }, + "counters": { + "rundeck.scheduler.quartz.scheduledJobs": { + "count": 6 + } + }, + "histograms": {}, + "meters": { + "rundeck.services.AuthorizationService.systemAuthorization.evaluateMeter": { + "count": 4, + "m15_rate": 0.00314076610191179, + "m1_rate": 0.023875601445527157, + "m5_rate": 0.008400048550624787, + "mean_rate": 0.026528128813374685, + "units": "events/second" + }, + "rundeck.services.AuthorizationService.systemAuthorization.evaluateSetMeter": { + "count": 12, + "m15_rate": 0.6892782713428792, + "m1_rate": 0.1267495745218626, + "m5_rate": 0.5153224625291539, + "mean_rate": 0.07958452971073966, + "units": "events/second" + }, + "rundeck.services.ExecutionService.executionJobStartMeter": { + "count": 6, + "m15_rate": 0.3446391356714396, + "m1_rate": 0.0633747872609313, + "m5_rate": 0.25766123126457696, + "mean_rate": 0.039926086575635206, + "units": "events/second" + }, + "rundeck.services.ExecutionService.executionStartMeter": { + "count": 6, + "m15_rate": 0.3446391356714396, + "m1_rate": 0.0633747872609313, + "m5_rate": 0.25766123126457696, + "mean_rate": 0.039893916635731115, + "units": "events/second" + }, + "rundeck.services.ExecutionService.executionSuccessMeter": { + "count": 6, + "m15_rate": 0.3465591259042392, + "m1_rate": 0.06888231291145262, + "m5_rate": 0.2619915710449402, + "mean_rate": 0.04041785210623091, + "units": "events/second" + } + }, + "timers": { + "rundeck.api.requests.requestTimer": { + "count": 3, + "max": 0.19907502000000002, + "mean": 0.108295424, + "min": 0.014703712, + "p50": 0.11110754, + "p75": 0.19907502000000002, + "p95": 0.19907502000000002, + "p98": 0.19907502000000002, + "p99": 0.19907502000000002, + "p999": 0.19907502000000002, + "stddev": 0.09221781715431031, + "m15_rate": 0.00314076610191179, + "m1_rate": 0.023875601445527157, + "m5_rate": 0.008400048550624787, + "mean_rate": 0.0326409948656659, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.quartzjobs.ExecutionJob.executionTimer": { + "count": 6, + "max": 2.785892703, + "mean": 0.6245617408333334, + "min": 0.156943942, + "p50": 0.2009052745, + "p75": 0.8750149552500001, + "p95": 2.785892703, + "p98": 2.785892703, + "p99": 2.785892703, + "p999": 2.785892703, + "stddev": 1.0593646577233937, + "m15_rate": 0.17537122122178622, + "m1_rate": 0.049487919338571586, + "m5_rate": 0.13657375399032906, + "mean_rate": 0.039744906881515114, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.services.AuthorizationService.getSystemAuthorization": { + "count": 10, + "max": 0.11291242300000001, + "mean": 0.0168355508, + "min": 0.002907568, + "p50": 0.003709257, + "p75": 0.0103103045, + "p95": 0.11291242300000001, + "p98": 0.11291242300000001, + "p99": 0.11291242300000001, + "p999": 0.11291242300000001, + "stddev": 0.0345229796390412, + "m15_rate": 0.3477799017733514, + "m1_rate": 0.08725038870645847, + "m5_rate": 0.2660612798152018, + "mean_rate": 0.06628145573313499, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.services.AuthorizationService.systemAuthorization.evaluateSetTimer": { + "count": 12, + "max": 0.064324482, + "mean": 0.0064664489166666676, + "min": 0.0006582410000000001, + "p50": 0.001099722, + "p75": 0.0018917270000000002, + "p95": 0.064324482, + "p98": 0.064324482, + "p99": 0.064324482, + "p999": 0.064324482, + "stddev": 0.01822988971428955, + "m15_rate": 0.6892782713428792, + "m1_rate": 0.1267495745218626, + "m5_rate": 0.5153224625291539, + "mean_rate": 0.07958252469001104, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.services.AuthorizationService.systemAuthorization.evaluateTimer": { + "count": 4, + "max": 0.002291996, + "mean": 0.00132195675, + "min": 0.000826429, + "p50": 0.001084701, + "p75": 0.00204558875, + "p95": 0.002291996, + "p98": 0.002291996, + "p99": 0.002291996, + "p999": 0.002291996, + "stddev": 0.0006824895873415091, + "m15_rate": 0.00314076610191179, + "m1_rate": 0.023875601445527157, + "m5_rate": 0.008400048550624787, + "mean_rate": 0.0265274537259422, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.services.FrameworkService.authorizeApplicationResource": { + "count": 4, + "max": 0.016328387, + "mean": 0.004882139000000001, + "min": 0.000912978, + "p50": 0.0011435955, + "p75": 0.012589778000000001, + "p95": 0.016328387, + "p98": 0.016328387, + "p99": 0.016328387, + "p999": 0.016328387, + "stddev": 0.007633923731977984, + "m15_rate": 0.3711760292127013, + "m1_rate": 0.14055240663997448, + "m5_rate": 0.32006153577038915, + "mean_rate": 0.05025453574530793, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.services.FrameworkService.filterNodeSet": { + "count": 12, + "max": 0.36586338300000004, + "mean": 0.03359687208333334, + "min": 0.000560871, + "p50": 0.0010942600000000001, + "p75": 0.00812147625, + "p95": 0.36586338300000004, + "p98": 0.36586338300000004, + "p99": 0.36586338300000004, + "p999": 0.36586338300000004, + "stddev": 0.10477591919675994, + "m15_rate": 0.6892782713428792, + "m1_rate": 0.1267495745218626, + "m5_rate": 0.5153224625291539, + "mean_rate": 0.07994704576896616, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.services.NodeService.project.bingo.loadNodes": { + "count": 3, + "max": 0.29763018, + "mean": 0.13177108533333334, + "min": 0.046235376, + "p50": 0.051447700000000006, + "p75": 0.29763018, + "p95": 0.29763018, + "p98": 0.29763018, + "p99": 0.29763018, + "p999": 0.29763018, + "stddev": 0.14366183050172013, + "m15_rate": 0.17327375280520316, + "m1_rate": 0.033814570567886885, + "m5_rate": 0.1309493035278718, + "mean_rate": 0.020034416530604948, + "duration_units": "seconds", + "rate_units": "calls/second" + }, + "rundeck.web.requests.requestTimer": { + "count": 5, + "max": 0.19833273, + "mean": 0.0921360348, + "min": 0.008643088, + "p50": 0.106350626, + "p75": 0.16599702400000002, + "p95": 0.19833273, + "p98": 0.19833273, + "p99": 0.19833273, + "p999": 0.19833273, + "stddev": 0.08113047507342931, + "m15_rate": 0.33800395660416016, + "m1_rate": 0.05334571472303017, + "m5_rate": 0.24315644263411573, + "mean_rate": 0.02965980629218819, + "duration_units": "seconds", + "rate_units": "calls/second" + } + } +}''' + ).addHeader('content-type', 'application/json') + ); + server.start() + + def retrofit = new Retrofit.Builder().baseUrl(server.url('/api/25/')). + addConverterFactory(JacksonConverterFactory.create()). + build() + def api = retrofit.create(RundeckApi) + + when: + def body = api.getMetricsData().execute().body() + + then: + RecordedRequest request1 = server.takeRequest() + request1.path == '/api/25/metrics/metrics' + + body.version == "3.1.3" + body.gauges.size() == 27 + body.gauges."rundeck.services.AuthorizationService.sourceCache.hitCount".value == 9 + body.gauges."rundeck.services.AuthorizationService.sourceCache.hitRate".value == 0.9 + body.gauges."rundeck.services.NodeService.nodeCache.hitRate".value == 0.9166666666666666 + body.counters.size() == 1 + body.counters."rundeck.scheduler.quartz.scheduledJobs".count == 6 + body.histograms.size() == 0 + body.meters.size() == 5 + body.meters."rundeck.services.AuthorizationService.systemAuthorization.evaluateMeter".count == 4 + body.meters."rundeck.services.AuthorizationService.systemAuthorization.evaluateMeter".m1_rate == 0.023875601445527157 + body.meters."rundeck.services.AuthorizationService.systemAuthorization.evaluateMeter".units == "events/second" + body.timers.size() == 9 + body.timers."rundeck.services.AuthorizationService.getSystemAuthorization".count == 10 + body.timers."rundeck.services.AuthorizationService.getSystemAuthorization".mean == 0.0168355508 + body.timers."rundeck.services.AuthorizationService.getSystemAuthorization".duration_units == "seconds" + server.shutdown() + } + +} diff --git a/src/test/groovy/org/rundeck/client/tool/commands/SchedulerSpec.groovy b/src/test/groovy/org/rundeck/client/tool/commands/SchedulerSpec.groovy index 8a98ef39..522faa15 100644 --- a/src/test/groovy/org/rundeck/client/tool/commands/SchedulerSpec.groovy +++ b/src/test/groovy/org/rundeck/client/tool/commands/SchedulerSpec.groovy @@ -20,20 +20,14 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import org.rundeck.client.api.RundeckApi -import org.rundeck.client.api.model.* import org.rundeck.client.api.model.scheduler.SchedulerTakeover -import org.rundeck.client.tool.RdApp -import org.rundeck.client.util.Client -import org.rundeck.client.util.RdClientConfig -import org.rundeck.toolbelt.CommandOutput import retrofit2.Retrofit import retrofit2.converter.jackson.JacksonConverterFactory -import retrofit2.mock.Calls import spock.lang.Specification /** - * @author greg - * @since 12/5/16 + * @author ahormazabal + * @since 2018/11/26 */ class SchedulerSpec extends Specification { From ba80438330372a0a3383c08204c65356618bb7af Mon Sep 17 00:00:00 2001 From: Alberto Hormazabal Date: Mon, 3 Dec 2018 16:30:55 -0300 Subject: [PATCH 2/4] typo on metric data api url --- .../src/main/java/org/rundeck/client/api/RundeckApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java index c6727aea..50e4fa10 100644 --- a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java +++ b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java @@ -1186,7 +1186,7 @@ Call bulkDisableJobSchedule( Call getPing(); /** - * @see API + * @see API */ @Headers("Accept: application/json") @GET("metrics/metrics") From bdb48cf701654010f19a24373804ff04f1645d7d Mon Sep 17 00:00:00 2001 From: Alberto Hormazabal Date: Mon, 17 Dec 2018 19:52:31 -0300 Subject: [PATCH 3/4] option to fail if unhealthy checks found --- .../rundeck/client/tool/commands/Metrics.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/rundeck/client/tool/commands/Metrics.java b/src/main/java/org/rundeck/client/tool/commands/Metrics.java index 72f13554..98558780 100644 --- a/src/main/java/org/rundeck/client/tool/commands/Metrics.java +++ b/src/main/java/org/rundeck/client/tool/commands/Metrics.java @@ -79,25 +79,26 @@ interface HealthCheckOptions { description = "Show only checks with unhealthy status.") boolean isUnhealthyOnly(); + @Option(shortName = "f", + longName = "fail", + description = "Exit with unsuccessful status if unhealthy checks are found.") + boolean failOnUnhealthy(); + } @Command(description = "Print health check status information.") - public void healthcheck(HealthCheckOptions options, CommandOutput output) throws IOException, InputError { + public boolean healthcheck(HealthCheckOptions options, CommandOutput output) throws IOException, InputError { Map healthCheckStatus = apiCall(RundeckApi::getHealthCheckMetrics); - Map statusList; - - if (options.isUnhealthyOnly()) { - statusList = healthCheckStatus.entrySet().stream() - .filter(entry -> !entry.getValue().isHealthy()) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue)); - } - else { - statusList = healthCheckStatus; - } + // Obtain unhealthy list. + Map unhealthyList = healthCheckStatus.entrySet().stream() + .filter(entry -> !entry.getValue().isHealthy()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue)); + + Map statusList = options.isUnhealthyOnly() ? unhealthyList : healthCheckStatus; if (statusList.size() > 0) { output.info("Showing current health status for " + statusList.size() + " checks:"); @@ -118,6 +119,8 @@ public void healthcheck(HealthCheckOptions options, CommandOutput output) throws else { output.warning("No results found."); } + + return !options.failOnUnhealthy() || unhealthyList.size() == 0 } // rd metrics threads From fe8680410188244854bace6dc7f2d2b6e984e63a Mon Sep 17 00:00:00 2001 From: Alberto Hormazabal Date: Mon, 17 Dec 2018 19:57:44 -0300 Subject: [PATCH 4/4] add missing semicolon on option to fail if unhealthy checks found --- src/main/java/org/rundeck/client/tool/commands/Metrics.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/rundeck/client/tool/commands/Metrics.java b/src/main/java/org/rundeck/client/tool/commands/Metrics.java index 98558780..3a488820 100644 --- a/src/main/java/org/rundeck/client/tool/commands/Metrics.java +++ b/src/main/java/org/rundeck/client/tool/commands/Metrics.java @@ -120,7 +120,7 @@ public boolean healthcheck(HealthCheckOptions options, CommandOutput output) thr output.warning("No results found."); } - return !options.failOnUnhealthy() || unhealthyList.size() == 0 + return !options.failOnUnhealthy() || unhealthyList.size() == 0; } // rd metrics threads