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}