|
| 1 | +package io.argus.cli.command; |
| 2 | + |
| 3 | +import io.argus.cli.config.CliConfig; |
| 4 | +import io.argus.cli.config.Messages; |
| 5 | +import io.argus.cli.provider.ProviderRegistry; |
| 6 | +import io.argus.cli.render.AnsiStyle; |
| 7 | +import io.argus.cli.render.RichRenderer; |
| 8 | +import io.argus.core.command.CommandGroup; |
| 9 | + |
| 10 | +import java.io.IOException; |
| 11 | +import java.io.InputStream; |
| 12 | +import java.util.ArrayList; |
| 13 | +import java.util.List; |
| 14 | +import java.util.Properties; |
| 15 | + |
| 16 | +/** |
| 17 | + * Explains JVM metrics, GC causes, and flags in plain English. |
| 18 | + */ |
| 19 | +public final class ExplainCommand implements Command { |
| 20 | + |
| 21 | + private static final int WIDTH = RichRenderer.DEFAULT_WIDTH; |
| 22 | + |
| 23 | + private static final Properties KB = loadKnowledgeBase(); |
| 24 | + |
| 25 | + private static Properties loadKnowledgeBase() { |
| 26 | + Properties p = new Properties(); |
| 27 | + try (InputStream in = ExplainCommand.class.getResourceAsStream("/explain.properties")) { |
| 28 | + if (in != null) { |
| 29 | + p.load(in); |
| 30 | + } |
| 31 | + } catch (IOException e) { |
| 32 | + // empty knowledge base — explain will show "no match" |
| 33 | + } |
| 34 | + return p; |
| 35 | + } |
| 36 | + |
| 37 | + @Override |
| 38 | + public String name() { return "explain"; } |
| 39 | + |
| 40 | + @Override |
| 41 | + public CommandGroup group() { return CommandGroup.PROFILING; } |
| 42 | + |
| 43 | + @Override |
| 44 | + public CommandMode mode() { return CommandMode.READ; } |
| 45 | + |
| 46 | + @Override |
| 47 | + public String description(Messages messages) { |
| 48 | + return messages.get("cmd.explain.desc"); |
| 49 | + } |
| 50 | + |
| 51 | + @Override |
| 52 | + public void execute(String[] args, CliConfig config, ProviderRegistry registry, Messages messages) { |
| 53 | + if (args.length == 0) { |
| 54 | + System.err.println("Usage: argus explain <term>"); |
| 55 | + System.err.println("Examples:"); |
| 56 | + System.err.println(" argus explain \"G1 Evacuation Pause\""); |
| 57 | + System.err.println(" argus explain -XX:MaxGCPauseMillis"); |
| 58 | + System.err.println(" argus explain throughput"); |
| 59 | + System.err.println(" argus explain gc-overhead"); |
| 60 | + return; |
| 61 | + } |
| 62 | + |
| 63 | + boolean json = "json".equals(config.format()); |
| 64 | + boolean useColor = config.color(); |
| 65 | + |
| 66 | + // Collect the query (join remaining non-option args) |
| 67 | + StringBuilder queryBuilder = new StringBuilder(); |
| 68 | + for (String arg : args) { |
| 69 | + if (arg.equals("--format=json")) { |
| 70 | + json = true; |
| 71 | + } else if (!arg.startsWith("--")) { |
| 72 | + if (queryBuilder.length() > 0) queryBuilder.append(' '); |
| 73 | + queryBuilder.append(arg); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + String query = queryBuilder.toString().trim(); |
| 78 | + if (query.isEmpty()) { |
| 79 | + System.err.println("Usage: argus explain <term>"); |
| 80 | + return; |
| 81 | + } |
| 82 | + |
| 83 | + // Look up: exact match first, then fuzzy |
| 84 | + String exactKey = "explain." + query; |
| 85 | + String explanation = KB.getProperty(exactKey); |
| 86 | + String matchedTerm = query; |
| 87 | + |
| 88 | + if (explanation == null) { |
| 89 | + // Fuzzy: find all keys whose suffix contains the query (case-insensitive) |
| 90 | + String lowerQuery = query.toLowerCase(); |
| 91 | + List<String> fuzzyMatches = new ArrayList<>(); |
| 92 | + for (String key : KB.stringPropertyNames()) { |
| 93 | + if (!key.startsWith("explain.")) continue; |
| 94 | + String term = key.substring("explain.".length()); |
| 95 | + if (term.toLowerCase().contains(lowerQuery)) { |
| 96 | + fuzzyMatches.add(term); |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + if (fuzzyMatches.size() == 1) { |
| 101 | + matchedTerm = fuzzyMatches.get(0); |
| 102 | + explanation = KB.getProperty("explain." + matchedTerm); |
| 103 | + } else if (fuzzyMatches.size() > 1) { |
| 104 | + if (json) { |
| 105 | + printJsonSuggestions(query, fuzzyMatches); |
| 106 | + } else { |
| 107 | + printSuggestions(useColor, query, fuzzyMatches); |
| 108 | + } |
| 109 | + return; |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + if (explanation == null) { |
| 114 | + if (json) { |
| 115 | + System.out.println("{\"query\":\"" + RichRenderer.escapeJson(query) |
| 116 | + + "\",\"found\":false,\"explanation\":null}"); |
| 117 | + } else { |
| 118 | + System.out.println(RichRenderer.boxHeader(useColor, "explain", WIDTH, "\"" + query + "\"")); |
| 119 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 120 | + System.out.println(RichRenderer.boxLine( |
| 121 | + AnsiStyle.style(useColor, AnsiStyle.YELLOW) + "No explanation found for: " + query |
| 122 | + + AnsiStyle.style(useColor, AnsiStyle.RESET), WIDTH)); |
| 123 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 124 | + System.out.println(RichRenderer.boxLine("Try: argus explain gc-overhead", WIDTH)); |
| 125 | + System.out.println(RichRenderer.boxLine(" argus explain throughput", WIDTH)); |
| 126 | + System.out.println(RichRenderer.boxLine(" argus explain \"G1 Evacuation Pause\"", WIDTH)); |
| 127 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 128 | + System.out.println(RichRenderer.boxFooter(useColor, null, WIDTH)); |
| 129 | + } |
| 130 | + return; |
| 131 | + } |
| 132 | + |
| 133 | + if (json) { |
| 134 | + printJson(query, matchedTerm, explanation); |
| 135 | + return; |
| 136 | + } |
| 137 | + |
| 138 | + printExplanation(useColor, matchedTerm, explanation); |
| 139 | + } |
| 140 | + |
| 141 | + private static void printExplanation(boolean useColor, String term, String explanation) { |
| 142 | + System.out.println(RichRenderer.boxHeader(useColor, "explain", WIDTH, "\"" + term + "\"")); |
| 143 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 144 | + |
| 145 | + // Term name in bold |
| 146 | + String bold = AnsiStyle.style(useColor, AnsiStyle.BOLD); |
| 147 | + String reset = AnsiStyle.style(useColor, AnsiStyle.RESET); |
| 148 | + System.out.println(RichRenderer.boxLine(bold + term + reset, WIDTH)); |
| 149 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 150 | + |
| 151 | + // Word-wrap explanation at (WIDTH - 4) characters |
| 152 | + int wrapAt = WIDTH - 4; |
| 153 | + for (String line : wordWrap(explanation, wrapAt)) { |
| 154 | + System.out.println(RichRenderer.boxLine(line, WIDTH)); |
| 155 | + } |
| 156 | + |
| 157 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 158 | + System.out.println(RichRenderer.boxFooter(useColor, null, WIDTH)); |
| 159 | + } |
| 160 | + |
| 161 | + private static void printSuggestions(boolean useColor, String query, List<String> matches) { |
| 162 | + System.out.println(RichRenderer.boxHeader(useColor, "explain", WIDTH, "\"" + query + "\"")); |
| 163 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 164 | + System.out.println(RichRenderer.boxLine( |
| 165 | + AnsiStyle.style(useColor, AnsiStyle.YELLOW) + "Multiple matches found:" |
| 166 | + + AnsiStyle.style(useColor, AnsiStyle.RESET), WIDTH)); |
| 167 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 168 | + for (String match : matches) { |
| 169 | + System.out.println(RichRenderer.boxLine( |
| 170 | + " " + AnsiStyle.style(useColor, AnsiStyle.CYAN) + match |
| 171 | + + AnsiStyle.style(useColor, AnsiStyle.RESET), WIDTH)); |
| 172 | + } |
| 173 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 174 | + System.out.println(RichRenderer.boxLine("Use a more specific term to get a full explanation.", WIDTH)); |
| 175 | + System.out.println(RichRenderer.emptyLine(WIDTH)); |
| 176 | + System.out.println(RichRenderer.boxFooter(useColor, null, WIDTH)); |
| 177 | + } |
| 178 | + |
| 179 | + private static void printJson(String query, String term, String explanation) { |
| 180 | + System.out.println("{\"query\":\"" + RichRenderer.escapeJson(query) |
| 181 | + + "\",\"found\":true" |
| 182 | + + ",\"term\":\"" + RichRenderer.escapeJson(term) + "\"" |
| 183 | + + ",\"explanation\":\"" + RichRenderer.escapeJson(explanation) + "\"}"); |
| 184 | + } |
| 185 | + |
| 186 | + private static void printJsonSuggestions(String query, List<String> matches) { |
| 187 | + StringBuilder sb = new StringBuilder(); |
| 188 | + sb.append("{\"query\":\"").append(RichRenderer.escapeJson(query)) |
| 189 | + .append("\",\"found\":false,\"suggestions\":["); |
| 190 | + for (int i = 0; i < matches.size(); i++) { |
| 191 | + if (i > 0) sb.append(','); |
| 192 | + sb.append('"').append(RichRenderer.escapeJson(matches.get(i))).append('"'); |
| 193 | + } |
| 194 | + sb.append("]}"); |
| 195 | + System.out.println(sb); |
| 196 | + } |
| 197 | + |
| 198 | + /** Splits text into lines no longer than maxWidth, breaking on spaces. */ |
| 199 | + private static List<String> wordWrap(String text, int maxWidth) { |
| 200 | + List<String> lines = new ArrayList<>(); |
| 201 | + String[] words = text.split(" "); |
| 202 | + StringBuilder current = new StringBuilder(); |
| 203 | + for (String word : words) { |
| 204 | + if (current.length() == 0) { |
| 205 | + current.append(word); |
| 206 | + } else if (current.length() + 1 + word.length() <= maxWidth) { |
| 207 | + current.append(' ').append(word); |
| 208 | + } else { |
| 209 | + lines.add(current.toString()); |
| 210 | + current = new StringBuilder(word); |
| 211 | + } |
| 212 | + } |
| 213 | + if (current.length() > 0) { |
| 214 | + lines.add(current.toString()); |
| 215 | + } |
| 216 | + return lines; |
| 217 | + } |
| 218 | +} |
0 commit comments