diff --git a/README.md b/README.md index d17f69a2..89ee4635 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,11 @@ Elasticsearch, Kibana, and Logstash each have three distinct execution modes ava Option only - no value. + + --includeTrends + Collect a 7-day CPU/heap trend summary from monitoring data (.monitoring-es-*), broken down by node and by day, if monitoring is enabled on the target cluster. Adds one additional query against the monitored cluster. Default value is false. + Option only - no value. + #### PKI Authentication Options diff --git a/src/main/java/co/elastic/support/diagnostics/DiagnosticInputs.java b/src/main/java/co/elastic/support/diagnostics/DiagnosticInputs.java index 469f0e44..c9242c37 100644 --- a/src/main/java/co/elastic/support/diagnostics/DiagnosticInputs.java +++ b/src/main/java/co/elastic/support/diagnostics/DiagnosticInputs.java @@ -106,6 +106,7 @@ public class DiagnosticInputs extends ElasticRestClientInputs { public final static String knownHostsDescription = "Known hosts file to search for target server. Default is ~/.ssh/known_hosts for Linux/Mac. Windows users should always set this explicitly."; public final static String sudoDescription = "Use sudo for remote commands? If not used, log retrieval and some system calls may fail."; public final static String remotePortDescription = "SSH port for the host being queried."; + public final static String includeTrendsDescription = "Collect a 7-day CPU/heap trend summary from monitoring data (.monitoring-es-*), if present. Adds one additional query against the monitored cluster."; // Input Fields @Parameter(names = { @@ -133,6 +134,8 @@ public class DiagnosticInputs extends ElasticRestClientInputs { public String knownHostsFile = ""; @Parameter(names = { "--sudo" }, description = sudoDescription) public boolean isSudo = false; + @Parameter(names = { "--includeTrends" }, description = includeTrendsDescription) + public boolean includeTrends = false; @Parameter(names = { "--remotePort" }, description = remotePortDescription) public int remotePort = 22; // End Input Fields diff --git a/src/main/java/co/elastic/support/diagnostics/chain/DiagnosticChainExec.java b/src/main/java/co/elastic/support/diagnostics/chain/DiagnosticChainExec.java index 533fdc7d..87f63794 100644 --- a/src/main/java/co/elastic/support/diagnostics/chain/DiagnosticChainExec.java +++ b/src/main/java/co/elastic/support/diagnostics/chain/DiagnosticChainExec.java @@ -14,6 +14,7 @@ import co.elastic.support.diagnostics.commands.CheckPlatformDetails; import co.elastic.support.diagnostics.commands.CheckUserAuthLevel; import co.elastic.support.diagnostics.commands.CollectDockerInfo; +import co.elastic.support.diagnostics.commands.CollectMonitoringTrends; import co.elastic.support.diagnostics.commands.CollectKibanaLogs; import co.elastic.support.diagnostics.commands.CollectLogs; import co.elastic.support.diagnostics.commands.CollectSystemCalls; @@ -39,6 +40,7 @@ public static void runDiagnostic(DiagnosticContext context, String type) throws // Removed temporarily due to issues with finding and accessing cloud master // new CheckPlatformDetails().execute(context); new RunClusterQueries().execute(context); + new CollectMonitoringTrends().execute(context); break; case Constants.local: @@ -46,6 +48,7 @@ public static void runDiagnostic(DiagnosticContext context, String type) throws new CheckUserAuthLevel().execute(context); new CheckPlatformDetails().execute(context); new RunClusterQueries().execute(context); + new CollectMonitoringTrends().execute(context); if (context.runSystemCalls) { new CollectSystemCalls().execute(context); new CollectLogs().execute(context); @@ -61,6 +64,7 @@ public static void runDiagnostic(DiagnosticContext context, String type) throws new CheckUserAuthLevel().execute(context); new CheckPlatformDetails().execute(context); new RunClusterQueries().execute(context); + new CollectMonitoringTrends().execute(context); if (context.runSystemCalls) { new CollectSystemCalls().execute(context); new CollectLogs().execute(context); diff --git a/src/main/java/co/elastic/support/diagnostics/commands/CollectMonitoringTrends.java b/src/main/java/co/elastic/support/diagnostics/commands/CollectMonitoringTrends.java new file mode 100644 index 00000000..5d4da3f5 --- /dev/null +++ b/src/main/java/co/elastic/support/diagnostics/commands/CollectMonitoringTrends.java @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package co.elastic.support.diagnostics.commands; + +import co.elastic.support.Constants; +import co.elastic.support.diagnostics.chain.Command; +import co.elastic.support.diagnostics.chain.DiagnosticContext; +import co.elastic.support.rest.RestClient; +import co.elastic.support.rest.RestResult; +import co.elastic.support.util.JsonYamlUtils; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; + +public class CollectMonitoringTrends implements Command { + + private static final Logger logger = LogManager.getLogger(CollectMonitoringTrends.class); + + private static final String AGG_QUERY = "{" + + "\"size\": 0," + + "\"timeout\": \"10s\"," + + "\"query\": { \"bool\": { \"filter\": [" + + " { \"term\": { \"type\": \"node_stats\" } }," + + " { \"range\": { \"timestamp\": { \"gte\": \"now-7d\" } } }" + + "] } }," + + "\"aggs\": {" + + " \"by_node\": {" + + " \"terms\": { \"field\": \"source_node.name\", \"size\": 50 }," + + " \"aggs\": {" + + " \"cpu_pct\": { \"percentiles\": { \"field\": \"node_stats.process.cpu.percent\", \"percents\": [50, 95, 99] } }," + + " \"heap_pct\": { \"percentiles\": { \"field\": \"node_stats.jvm.mem.heap_used_percent\", \"percents\": [50, 95, 99] } }," + + " \"by_day\": {" + + " \"date_histogram\": { \"field\": \"timestamp\", \"fixed_interval\": \"1d\" }," + + " \"aggs\": {" + + " \"cpu_pct\": { \"percentiles\": { \"field\": \"node_stats.process.cpu.percent\", \"percents\": [50, 95, 99] } }," + + " \"heap_pct\": { \"percentiles\": { \"field\": \"node_stats.jvm.mem.heap_used_percent\", \"percents\": [50, 95, 99] } }" + + " }" + + " }" + + " }" + + " }" + + "}" + + "}"; + + public void execute(DiagnosticContext context) { + if (!context.diagnosticInputs.includeTrends) { + return; + } + + try { + RestClient client = context.resourceCache.getRestClient(Constants.restInputHost); + + // 1) Check whether monitoring indices exist at all - skip quietly if not. + RestResult checkResult = client.execQuery("/.monitoring-es-*/_search?size=0"); + JsonNode checkNode = JsonYamlUtils.createJsonNodeFromString(checkResult.toString()); + long totalShards = checkNode.path("_shards").path("total").asLong(0); + + if (totalShards == 0) { + logger.info(Constants.CONSOLE, "No monitoring indices found - skipping trend summary."); + return; + } + + // 2) Run the aggregation query. + HttpResponse response = client.execPost("/.monitoring-es-*/_search", AGG_QUERY); + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + String body = entity != null ? EntityUtils.toString(entity) : ""; + + if (status < 200 || status >= 300) { + logger.info(Constants.CONSOLE, "Monitoring trend query failed (status {}) - skipping.", status); + return; + } + + // 3) Write the result to the diagnostic output directory. + File outFile = new File(context.tempDir, "monitoring-trends.json"); + FileUtils.writeStringToFile(outFile, body, "UTF-8"); + logger.info(Constants.CONSOLE, "Monitoring trend summary written to: {}", outFile.getName()); + + } catch (Exception e) { + // This feature failing should never block the rest of the diagnostic. + logger.info(Constants.CONSOLE, "Could not collect monitoring trend summary - bypassing."); + logger.error("Error collecting monitoring trends", e); + } + } +}