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 private long sleepTime = 60 * 60 * 1000; 061 private long drift = 0; 062 private boolean detailRequired = false; 063 private Thread me = null; 064 private static final int MB = 1024*1024; 065 private String[] scripts; 066 private String frozenDump; 067 private String localHost; 068 private String processName; 069 private Lock dumping = new ReentrantLock(); 070 @Config("metrics-dir") 071 private String metricsDir; 072 073 @Config("dump-stacktrace") 074 boolean dumpStackTrace; 075 076 int stackTraceDepth; 077 @Override 078 public void initService() { } 079 080 @Override 081 public void startService() { 082 try { 083 me = Thread.ofVirtual().name("SystemMonitor").start(this); 084 } catch (Exception e) { 085 log.warn("error starting service", e); 086 } 087 } 088 089 public void stopService() { 090 interruptMainThread(); 091 } 092 public void destroyService() throws InterruptedException { 093 me.join(Duration.ofMillis(5000L)); 094 } 095 096 public synchronized void setSleepTime(long sleepTime) { 097 this.sleepTime = sleepTime; 098 setModified(true); 099 interruptMainThread(); 100 } 101 102 public synchronized long getSleepTime() { 103 return sleepTime; 104 } 105 106 public synchronized void setDetailRequired(boolean detail) { 107 this.detailRequired = detail; 108 setModified(true); 109 interruptMainThread(); 110 } 111 112 public synchronized boolean getDetailRequired() { 113 return detailRequired; 114 } 115 116 private List<KV> generateThreadInfo () { 117 List<KV> threads = new ArrayList<>(); 118 Thread.getAllStackTraces().entrySet().stream() 119 .sorted(Comparator.comparingLong(e -> e.getKey().threadId())) 120 .forEach((e -> { 121 Thread t = e.getKey(); 122 StackTraceElement[] stackTrace = e.getValue(); 123 String currentMethodInfo = stackTrace.length > 0 ? 124 "%s.%s(%s:%d)".formatted(stackTrace[0].getClassName(), stackTrace[0].getMethodName(), 125 stackTrace[0].getFileName(), stackTrace[0].getLineNumber()) : 126 ""; 127 threads.add(new KV( 128 "%s:%d".formatted(t.getThreadGroup(), t.threadId()), 129 "%s - %s".formatted(t.getName(), currentMethodInfo) 130 )); 131 })); 132 return threads; 133 } 134 135 public void run() { 136 localHost = getLocalHost(); 137 processName = ManagementFactory.getRuntimeMXBean().getName(); 138 while (running()) { 139 dumpSystemInfo(); 140 try { 141 long expected = System.currentTimeMillis() + sleepTime; 142 Thread.sleep(sleepTime); 143 drift = System.currentTimeMillis() - expected; 144 } catch (InterruptedException ignored) { } 145 } 146 dumpSystemInfo(); 147 } 148 149 @Override 150 public void setConfiguration(Configuration cfg) throws ConfigurationException { 151 super.setConfiguration(cfg); 152 scripts = cfg.getAll("script"); 153 stackTraceDepth = cfg.getInt("stacktrace-depth", Integer.MAX_VALUE); 154 } 155 156 private Runtime getRuntimeInstance() { 157 return Runtime.getRuntime(); 158 } 159 160 private long getServerUptimeAsMillisecond() { 161 return getServer().getUptime().toMillis(); 162 } 163 164 private String getInstanceIdAsString() { 165 return getServer().getInstanceId().toString(); 166 } 167 168 private String getRevision() { 169 return Q2.getRevision(); 170 } 171 private String getLocalHost () { 172 try { 173 return InetAddress.getLocalHost().toString(); 174 } catch (Exception e) { 175 return e.getMessage(); 176 } 177 } 178 private void exec (String script, PrintStream ps, String indent) { 179 try { 180 ProcessBuilder pb = new ProcessBuilder (QExec.parseCommandLine(script)); 181 Process p = pb.start(); 182 BufferedReader in = p.inputReader(); 183 String line; 184 while ((line = in.readLine()) != null) { 185 ps.printf("%s%s%n", indent, line); 186 } 187 p.waitFor(); 188 } catch (Exception e) { 189 e.printStackTrace(ps); 190 } 191 } 192 193 private double loadAverage () { 194 MBeanServerConnection mbsc = ManagementFactory.getPlatformMBeanServer(); 195 try { 196 OperatingSystemMXBean osMBean = ManagementFactory.newPlatformMXBeanProxy( 197 mbsc, ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME, OperatingSystemMXBean.class); 198 199 return osMBean.getSystemLoadAverage(); 200 } catch (Throwable ignored) { } 201 return -1; 202 } 203 204 private void dumpMetrics() { 205 if (metricsDir != null) { 206 File dir = new File(cfg.get("metrics-dir")); 207 dir.mkdir(); 208 NameRegistrar.getAsMap().forEach((key, value) -> { 209 if (value instanceof MetricsProvider) { 210 Metrics metrics = ((MetricsProvider) value).getMetrics(); 211 if (metrics != null) 212 metrics.dumpHistograms(dir, key + "-"); 213 } 214 }); 215 } 216 } 217 218 private void interruptMainThread() { 219 if (me != null) { 220 dumping.lock(); 221 try { 222 me.interrupt(); 223 } finally { 224 dumping.unlock(); 225 } 226 } 227 } 228 229 private LogEvent generateSystemInfo () { 230 LogEvent evt = new LogEvent(this.getLog(), "info") 231 .withTraceId(getServer().getInstanceId()); 232 List<AuditLogEvent> events = new ArrayList<>(); 233 RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 234 ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); 235 Runtime r = getRuntimeInstance(); 236 ZoneId zi = ZoneId.systemDefault(); 237 Instant instant = Instant.now(); 238 File cwd = new File("."); 239 String freeSpace = ISOUtil.readableFileSize(cwd.getFreeSpace()); 240 String usableSpace = ISOUtil.readableFileSize(cwd.getUsableSpace()); 241 int maxKeyLength = 0; 242 try { 243 maxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); 244 } catch (NoSuchAlgorithmException ignored) { } 245 246 long totalMemory = r.totalMemory(); 247 long freeMemory = r.freeMemory(); 248 long usedMemory = totalMemory - freeMemory; 249 long maxMemory = r.maxMemory(); 250 long gcTotalCnt = 0; 251 long gcTotalTime = 0; 252 for(GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { 253 gcTotalCnt += Math.max(gc.getCollectionCount(), 0L); 254 gcTotalTime += Math.max(gc.getCollectionTime(), 0L); 255 } 256 evt.addMessage (new SysInfo( 257 System.getProperty("os.name"), 258 System.getProperty("os.version"), 259 System.getProperty("java.version"), 260 System.getProperty("java.vendor"), 261 maxKeyLength == Integer.MAX_VALUE ? "secure" : Integer.toString(maxKeyLength), 262 localHost, 263 System.getProperty("user.name"), 264 System.getProperty("user.dir"), 265 getServer().getWatchServiceClassname(), 266 Environment.getEnvironment().getName(), 267 String.join(",", runtimeMXBean.getInputArguments()), 268 Charset.defaultCharset(), 269 String.format("%s (%s) %s %s%s", 270 zi, zi.getDisplayName(TextStyle.FULL, Locale.getDefault()), 271 zi.getRules().getOffset(instant), 272 zi.getRules().getTransitionRules().toString(), 273 Optional.ofNullable(zi.getRules().nextTransition(instant)) 274 .map(transition -> " " + transition) 275 .orElse("") 276 ), 277 processName, 278 freeSpace, 279 usableSpace, 280 Q2.getVersion(), 281 Q2.getRevision(), 282 getServer().getInstanceId(), 283 getServer().getUptime(), 284 loadAverage(), 285 r.availableProcessors(), 286 drift, 287 maxMemory/MB, 288 totalMemory/MB, 289 freeMemory/MB, 290 usedMemory/MB, 291 gcTotalCnt, 292 gcTotalTime, 293 mxBean.getThreadCount(), 294 mxBean.getPeakThreadCount(), 295 NameRegistrar.getAsMap() 296 .entrySet() 297 .stream() 298 .map(entry -> new KV(entry.getKey(), entry.getValue().toString())) 299 .collect(Collectors.toList()), 300 generateThreadInfo(), 301 runScripts() 302 )); 303 return evt; 304 305 } 306 307 private List<ProcessOutput> runScripts() { 308 List<ProcessOutput> l = new ArrayList<>(); 309 for (String s : scripts) { 310 l.add(exec(s)); 311 } 312 return l; 313 } 314 315 private ProcessOutput exec(String script) { 316 StringBuilder stdout = new StringBuilder(); 317 StringBuilder stderr = new StringBuilder(); 318 try { 319 ProcessBuilder pb = new ProcessBuilder(QExec.parseCommandLine(script)); 320 Process p = pb.start(); 321 // Capture standard output 322 try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { 323 stdout.append(reader.lines().collect(Collectors.joining(System.lineSeparator()))); 324 } 325 // Capture error output 326 try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getErrorStream()))) { 327 String errorOutput = reader.lines().collect(Collectors.joining(System.lineSeparator())); 328 if (!errorOutput.isEmpty()) { 329 stderr.append(reader.lines().collect(Collectors.joining(System.lineSeparator()))); 330 } 331 } 332 p.waitFor(); 333 } catch (Exception e) { 334 stderr.append("Exception: ").append(e.getMessage()); 335 e.printStackTrace(new PrintStream(new OutputStream() { 336 @Override 337 public void write(int b) { 338 stdout.append((char) b); 339 } 340 })); 341 } 342 return new ProcessOutput (script, stdout.toString(), stderr.isEmpty() ? null : stderr.toString()); 343 } 344 345 private void dumpSystemInfo () { 346 dumping.lock(); 347 try { 348 Logger.log(generateSystemInfo()); 349 } finally { 350 dumping.unlock(); 351 } 352 } 353}