001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.q2.qbean; 020 021import org.jpos.core.Configuration; 022import org.jpos.core.ConfigurationException; 023import org.jpos.core.Environment; 024import org.jpos.core.annotation.Config; 025import org.jpos.iso.ISOUtil; 026import org.jpos.log.AuditLogEvent; 027import org.jpos.log.evt.KV; 028import org.jpos.log.evt.ProcessOutput; 029import org.jpos.log.evt.SysInfo; 030import org.jpos.q2.Q2; 031import org.jpos.q2.QBeanSupport; 032import org.jpos.util.*; 033 034import javax.crypto.Cipher; 035import javax.management.MBeanServerConnection; 036import java.io.*; 037import java.lang.management.*; 038import java.net.InetAddress; 039import java.nio.charset.Charset; 040import java.security.NoSuchAlgorithmException; 041import java.time.Duration; 042import java.time.Instant; 043import java.time.ZoneId; 044import java.time.format.TextStyle; 045import java.util.*; 046import java.util.concurrent.locks.Lock; 047import java.util.concurrent.locks.ReentrantLock; 048import java.util.stream.Collectors; 049 050/** 051 * Periodically dumps Thread and memory usage 052 * 053 * @author apr@cs.com.uy 054 * @version $Id$ 055 * @see Logger 056 */ 057public class SystemMonitor extends QBeanSupport 058 implements Runnable, SystemMonitorMBean 059{ 060 /** Default constructor; no instance state to initialise. */ 061 public SystemMonitor() {} 062 private long sleepTime = 60 * 60 * 1000; 063 private long drift = 0; 064 private boolean detailRequired = false; 065 private Thread me = null; 066 private static final int MB = 1024*1024; 067 private String[] scripts; 068 private String frozenDump; 069 private String localHost; 070 private String processName; 071 private Lock dumping = new ReentrantLock(); 072 @Config("metrics-dir") 073 private String metricsDir; 074 075 @Config("dump-stacktrace") 076 boolean dumpStackTrace; 077 078 @Override 079 protected String defaultRealm() { 080 return Realm.Q2_SYSMON; 081 } 082 083 int stackTraceDepth; 084 @Override 085 public void initService() { } 086 087 @Override 088 public void startService() { 089 try { 090 me = Thread.ofVirtual().name("SystemMonitor").start(this); 091 } catch (Exception e) { 092 log.warn("error starting service", e); 093 } 094 } 095 096 public void stopService() { 097 interruptMainThread(); 098 } 099 public void destroyService() throws InterruptedException { 100 me.join(Duration.ofMillis(5000L)); 101 } 102 103 public synchronized void setSleepTime(long sleepTime) { 104 this.sleepTime = sleepTime; 105 setModified(true); 106 interruptMainThread(); 107 } 108 109 public synchronized long getSleepTime() { 110 return sleepTime; 111 } 112 113 public synchronized void setDetailRequired(boolean detail) { 114 this.detailRequired = detail; 115 setModified(true); 116 interruptMainThread(); 117 } 118 119 public synchronized boolean getDetailRequired() { 120 return detailRequired; 121 } 122 123 private List<KV> generateThreadInfo () { 124 List<KV> threads = new ArrayList<>(); 125 Thread.getAllStackTraces().entrySet().stream() 126 .sorted(Comparator.comparingLong(e -> e.getKey().threadId())) 127 .forEach((e -> { 128 Thread t = e.getKey(); 129 StackTraceElement[] stackTrace = e.getValue(); 130 String currentMethodInfo = stackTrace.length > 0 ? 131 "%s.%s(%s:%d)".formatted(stackTrace[0].getClassName(), stackTrace[0].getMethodName(), 132 stackTrace[0].getFileName(), stackTrace[0].getLineNumber()) : 133 ""; 134 threads.add(new KV( 135 "%s:%d".formatted(t.getThreadGroup(), t.threadId()), 136 "%s - %s".formatted(t.getName(), currentMethodInfo) 137 )); 138 })); 139 return threads; 140 } 141 142 public void run() { 143 localHost = getLocalHost(); 144 processName = ManagementFactory.getRuntimeMXBean().getName(); 145 while (running()) { 146 dumpSystemInfo(); 147 try { 148 long expected = System.currentTimeMillis() + sleepTime; 149 Thread.sleep(sleepTime); 150 drift = System.currentTimeMillis() - expected; 151 } catch (InterruptedException ignored) { } 152 } 153 dumpSystemInfo(); 154 } 155 156 @Override 157 public void setConfiguration(Configuration cfg) throws ConfigurationException { 158 super.setConfiguration(cfg); 159 scripts = cfg.getAll("script"); 160 stackTraceDepth = cfg.getInt("stacktrace-depth", Integer.MAX_VALUE); 161 } 162 163 private Runtime getRuntimeInstance() { 164 return Runtime.getRuntime(); 165 } 166 167 private long getServerUptimeAsMillisecond() { 168 return getServer().getUptime().toMillis(); 169 } 170 171 private String getInstanceIdAsString() { 172 return getServer().getInstanceId().toString(); 173 } 174 175 private String getRevision() { 176 return Q2.getRevision(); 177 } 178 private String getLocalHost () { 179 try { 180 return InetAddress.getLocalHost().toString(); 181 } catch (Exception e) { 182 return e.getMessage(); 183 } 184 } 185 private void exec (String script, PrintStream ps, String indent) { 186 try { 187 ProcessBuilder pb = new ProcessBuilder (QExec.parseCommandLine(script)); 188 Process p = pb.start(); 189 BufferedReader in = p.inputReader(); 190 String line; 191 while ((line = in.readLine()) != null) { 192 ps.printf("%s%s%n", indent, line); 193 } 194 p.waitFor(); 195 } catch (Exception e) { 196 e.printStackTrace(ps); 197 } 198 } 199 200 private double loadAverage () { 201 MBeanServerConnection mbsc = ManagementFactory.getPlatformMBeanServer(); 202 try { 203 OperatingSystemMXBean osMBean = ManagementFactory.newPlatformMXBeanProxy( 204 mbsc, ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME, OperatingSystemMXBean.class); 205 206 return osMBean.getSystemLoadAverage(); 207 } catch (Throwable ignored) { } 208 return -1; 209 } 210 211 private void dumpMetrics() { 212 if (metricsDir != null) { 213 File dir = new File(cfg.get("metrics-dir")); 214 dir.mkdir(); 215 NameRegistrar.getAsMap().forEach((key, value) -> { 216 if (value instanceof MetricsProvider) { 217 Metrics metrics = ((MetricsProvider) value).getMetrics(); 218 if (metrics != null) 219 metrics.dumpHistograms(dir, key + "-"); 220 } 221 }); 222 } 223 } 224 225 private void interruptMainThread() { 226 if (me != null) { 227 dumping.lock(); 228 try { 229 me.interrupt(); 230 } finally { 231 dumping.unlock(); 232 } 233 } 234 } 235 236 private LogEvent generateSystemInfo () { 237 LogEvent evt = new LogEvent(this.getLog(), "info") 238 .withTraceId(getServer().getInstanceId()); 239 List<AuditLogEvent> events = new ArrayList<>(); 240 RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 241 ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); 242 Runtime r = getRuntimeInstance(); 243 ZoneId zi = ZoneId.systemDefault(); 244 Instant instant = Instant.now(); 245 File cwd = new File("."); 246 String freeSpace = ISOUtil.readableFileSize(cwd.getFreeSpace()); 247 String usableSpace = ISOUtil.readableFileSize(cwd.getUsableSpace()); 248 int maxKeyLength = 0; 249 try { 250 maxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); 251 } catch (NoSuchAlgorithmException ignored) { } 252 253 long totalMemory = r.totalMemory(); 254 long freeMemory = r.freeMemory(); 255 long usedMemory = totalMemory - freeMemory; 256 long maxMemory = r.maxMemory(); 257 long gcTotalCnt = 0; 258 long gcTotalTime = 0; 259 for(GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { 260 gcTotalCnt += Math.max(gc.getCollectionCount(), 0L); 261 gcTotalTime += Math.max(gc.getCollectionTime(), 0L); 262 } 263 evt.addMessage (new SysInfo( 264 System.getProperty("os.name"), 265 System.getProperty("os.version"), 266 System.getProperty("java.version"), 267 System.getProperty("java.vendor"), 268 maxKeyLength == Integer.MAX_VALUE ? "secure" : Integer.toString(maxKeyLength), 269 localHost, 270 System.getProperty("user.name"), 271 System.getProperty("user.dir"), 272 getServer().getWatchServiceClassname(), 273 Environment.getEnvironment().getName(), 274 String.join(",", runtimeMXBean.getInputArguments()), 275 Charset.defaultCharset(), 276 String.format("%s (%s) %s %s%s", 277 zi, zi.getDisplayName(TextStyle.FULL, Locale.getDefault()), 278 zi.getRules().getOffset(instant), 279 zi.getRules().getTransitionRules().toString(), 280 Optional.ofNullable(zi.getRules().nextTransition(instant)) 281 .map(transition -> " " + transition) 282 .orElse("") 283 ), 284 processName, 285 freeSpace, 286 usableSpace, 287 Q2.getVersion(), 288 Q2.getRevision(), 289 Q2.getClassPath(), 290 Q2.getClassPathHash(), 291 getServer().getInstanceId(), 292 getServer().getUptime(), 293 loadAverage(), 294 r.availableProcessors(), 295 drift, 296 maxMemory/MB, 297 totalMemory/MB, 298 freeMemory/MB, 299 usedMemory/MB, 300 gcTotalCnt, 301 gcTotalTime, 302 mxBean.getThreadCount(), 303 mxBean.getPeakThreadCount(), 304 NameRegistrar.getAsMap() 305 .entrySet() 306 .stream() 307 .map(entry -> new KV(entry.getKey(), entry.getValue().toString())) 308 .collect(Collectors.toList()), 309 generateThreadInfo(), 310 runScripts() 311 )); 312 return evt; 313 314 } 315 316 private List<ProcessOutput> runScripts() { 317 List<ProcessOutput> l = new ArrayList<>(); 318 for (String s : scripts) { 319 l.add(exec(s)); 320 } 321 return l; 322 } 323 324 private ProcessOutput exec(String script) { 325 StringBuilder stdout = new StringBuilder(); 326 StringBuilder stderr = new StringBuilder(); 327 try { 328 ProcessBuilder pb = new ProcessBuilder(QExec.parseCommandLine(script)); 329 Process p = pb.start(); 330 // Capture standard output 331 try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { 332 stdout.append(reader.lines().collect(Collectors.joining(System.lineSeparator()))); 333 } 334 // Capture error output 335 try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getErrorStream()))) { 336 String errorOutput = reader.lines().collect(Collectors.joining(System.lineSeparator())); 337 if (!errorOutput.isEmpty()) { 338 stderr.append(reader.lines().collect(Collectors.joining(System.lineSeparator()))); 339 } 340 } 341 p.waitFor(); 342 } catch (Exception e) { 343 stderr.append("Exception: ").append(e.getMessage()); 344 e.printStackTrace(new PrintStream(new OutputStream() { 345 @Override 346 public void write(int b) { 347 stdout.append((char) b); 348 } 349 })); 350 } 351 return new ProcessOutput (script, stdout.toString(), stderr.isEmpty() ? null : stderr.toString()); 352 } 353 354 private void dumpSystemInfo () { 355 dumping.lock(); 356 try { 357 Logger.log(generateSystemInfo()); 358 } finally { 359 dumping.unlock(); 360 } 361 } 362}