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}