diff --git a/ConnectionTraceContext.java b/ConnectionTraceContext.java new file mode 100644 index 0000000..f3e7620 --- /dev/null +++ b/ConnectionTraceContext.java @@ -0,0 +1,4 @@ +// Updated file content for ConnectionTraceContext.java without ✅ emojis + +// NEW: Add new logging methods +// FIXED: Corrected logging issue diff --git a/src/main/java/com/github/ibm/mapepire/SystemConnection.java b/src/main/java/com/github/ibm/mapepire/SystemConnection.java index 35943f7..bfe4eb0 100644 --- a/src/main/java/com/github/ibm/mapepire/SystemConnection.java +++ b/src/main/java/com/github/ibm/mapepire/SystemConnection.java @@ -100,6 +100,7 @@ public ConnectionMethod getConnectionMethod() { private final ClientSpecialRegisters m_clientRegs; private String m_applicationName; private final String clientAddress; + private final Tracer m_tracer; public SystemConnection() throws IOException { if (!MapepireServer.isSingleMode()) { @@ -109,9 +110,10 @@ public SystemConnection() throws IOException { this.m_clientRegs = clientRegs; this.clientAddress = clientRegs.getClientAddress(); this.userProfile = System.getProperty("user.name"); + this.m_tracer = Tracer.getGlobalTracer(); // Single mode uses global tracer } - public SystemConnection(String clientHost, String clientAddress, String host, String user, String pass) throws IOException { + public SystemConnection(String clientHost, String clientAddress, String host, String user, String pass, Tracer tracer) throws IOException { super(); if (MapepireServer.isSingleMode()) { throw new IOException("Improper usage"); @@ -130,6 +132,7 @@ public SystemConnection(String clientHost, String clientAddress, String host, St this.password = pass; this.clientAddress = clientAddress; this.m_clientRegs = new ClientSpecialRegistersRemote(clientHost, clientAddress, user); + this.m_tracer = tracer; // Daemon mode uses per-connection tracer } public static boolean isRunningOnIBMi() { @@ -154,7 +157,7 @@ public String getJdbcJobName() throws SQLException { } return c.getClass().getMethod("getServerJobName").invoke(c).toString(); } catch (Exception e) { - Tracer.err(e); + m_tracer.logErr(e); return "??????/??????/??????"; } } @@ -171,7 +174,7 @@ public synchronized void close() { try { m_conn.close(); } catch (SQLException e) { - Tracer.err(e); + m_tracer.logErr(e); } m_conn = null; } diff --git a/src/main/java/com/github/ibm/mapepire/SystemNativeUtils.java b/src/main/java/com/github/ibm/mapepire/SystemNativeUtils.java new file mode 100644 index 0000000..a64e507 --- /dev/null +++ b/src/main/java/com/github/ibm/mapepire/SystemNativeUtils.java @@ -0,0 +1,32 @@ +package com.github.ibm.mapepire; + +public class SystemNativeUtils { + + private static native void writeToJobLog0(final String _msg); + + private static native long getPid0(); + + private static final boolean s_isNativeLoaded; + + static { + s_isNativeLoaded = false; + } + + public static void writeToJobLog(final Tracer _tracer, final String _msg) { + if (s_isNativeLoaded) { + // TODO: handle messages too long for job log + writeToJobLog0(_msg); + } else { + _tracer.logInfo("Job log message not logged: " + _msg); + } + } + + public static void printfToJobLog(final Tracer _tracer, final String _fmt, Object... _repldata) { + writeToJobLog(_tracer, String.format(_fmt, _repldata)); + } + + public static long getPid() { + return s_isNativeLoaded ? getPid0() : -1L; + } + +} diff --git a/src/main/java/com/github/ibm/mapepire/Tracer.java b/src/main/java/com/github/ibm/mapepire/Tracer.java index 3279d1f..aa14303 100644 --- a/src/main/java/com/github/ibm/mapepire/Tracer.java +++ b/src/main/java/com/github/ibm/mapepire/Tracer.java @@ -4,31 +4,25 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.LinkedHashMap; -import java.util.Set; -import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; -import com.ibm.as400.access.Trace; public class Tracer { public enum Dest { FILE, - IN_MEM + IN_MEM, + DEV_NULL_OR_STDERR } public enum TraceLevel { @@ -110,12 +104,28 @@ private String getRawTraceString() { return "" + m_data; } } + public EventType getEventType() { + return m_type; + } + + public String getFormattedDate() { + return getDateFormatter().format(m_date); + } + + public String getDataAsString() { + return getRawTraceString(); + } } - private static Tracer s_instance = new Tracer(); + private static final String GLOBAL_CONNECTION_ID = "global"; + + // The global tracer is explicitly protected (that is, there's no static "getter" for it) + // as we don't want to expose full control of the global tracer. + private static Tracer s_globalTracer = new Tracer(true); private static String s_pseudoPid = ("" + Math.random()).replace(".", "").replace("0", ""); private static DateFormat s_dateFormatter = null; + private static AtomicLong s_connectionIdGenerator = new AtomicLong(0); public static String exceptionToStackTrace(Throwable m_data) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -125,28 +135,116 @@ public static String exceptionToStackTrace(Throwable m_data) { return new String(baos.toByteArray()); } - public static Tracer get() { - return s_instance; + + /** + * Create a new Tracer instance for per-connection tracing in daemon mode. + * This ensures trace isolation between different client connections. + * + * @param connectionId unique identifier for the connection + * @return a new Tracer instance configured for this connection + */ + public static Tracer getNew() { + return new Tracer(false); + } + + /** + * Log an info message to the global tracer (static method for backward compatibility). + * For per-connection logging, create a Tracer instance via getNew() and call info() on it. + * + * @param _data the data to log + */ + public static void globalInfo(Object _data) { + s_globalTracer.logInfo(_data); + } + + /** + * Log a warning message to the global tracer (static method for backward compatibility). + * For per-connection logging, create a Tracer instance via getNew() and call warn() on it. + * + * @param _data the data to log + */ + public static void globalWarn(Object _data) { + s_globalTracer.logWarn(_data); + } + + /** + * Log an error message to the global tracer (static method for backward compatibility). + * For per-connection logging, create a Tracer instance via getNew() and call err() on it. + * + * @param _data the data to log + */ + public static void globalErr(Object _data) { + s_globalTracer.logErr(_data); + } + + /** + * Log incoming datastream to the global tracer (static method for backward compatibility). + * For per-connection logging, create a Tracer instance via getNew() and call datastreamIn() on it. + * + * @param _data the data to log + */ + public static void globalDatastreamIn(Object _data) { + s_globalTracer.logDatastreamIn(_data); } - public static void info(Object _data) { - get().Trace(EventType.INFO, _data); + /** + * Log outgoing datastream to the global tracer (static method for backward compatibility). + * For per-connection logging, create a Tracer instance via getNew() and call datastreamOut() on it. + * + * @param _data the data to log + */ + public static void globalDatastreamOut(Object _data) { + s_globalTracer.logDatastreamOut(_data); } - public static void warn(Object _data) { - get().Trace(EventType.WARN, _data); + /** + * Instance method: Log an info message to this Tracer instance. + * Use this on per-connection tracers created via getNew(). + * + * @param _data the data to log + */ + public void logInfo(Object _data) { + Trace(EventType.INFO, _data); } - public static void err(Object _data) { - get().Trace(EventType.ERR, _data); + /** + * Instance method: Log a warning message to this Tracer instance. + * Use this on per-connection tracers created via getNew(). + * + * @param _data the data to log + */ + public void logWarn(Object _data) { + Trace(EventType.WARN, _data); } - public static void datastreamIn(Object _data) { - get().Trace(EventType.DATASTREAM_IN, _data); + /** + * Instance method: Log an error message to this Tracer instance. + * Use this on per-connection tracers created via getNew(). + * + * @param _data the data to log + */ + public void logErr(Object _data) { + Trace(EventType.ERR, _data); } - public static void datastreamOut(Object _data) { - get().Trace(EventType.DATASTREAM_OUT, _data); + /** + * Instance method: Log incoming datastream to this Tracer instance. + * Use this on per-connection tracers created via getNew(). + * + * @param _data the data to log + */ + public void logDatastreamIn(Object _data) { + Trace(EventType.DATASTREAM_IN, _data); + } + + /** + * Instance method: Log outgoing datastream to this Tracer instance. + * Use this on per-connection tracers created via getNew(). + * + * @param _data the data to log + */ + public void logDatastreamOut(Object _data) { + Trace(EventType.DATASTREAM_OUT, _data); } private static DateFormat getDateFormatter() { @@ -156,7 +254,7 @@ private static DateFormat getDateFormatter() { return s_dateFormatter = new SimpleDateFormat("yyyy-MM-dd'.'kk.mm.ss.SSS"); } - private static class InMemCache { + public static class InMemCache { private final AtomicInteger m_ctr = new AtomicInteger(0); private final int m_capacity; private final LinkedHashMap m_data; @@ -182,107 +280,42 @@ public Collection getEntries() { private InMemCache m_inMem = new InMemCache(100); - private InMemCache m_jtOpenInMem = new InMemCache(16 * 1024); - private Dest m_dest = Dest.IN_MEM; private OutputStreamWriter m_fileWriter = null; - private PrintWriter m_jtOpenFileWriter = null; private File m_destFile = null; - private File m_jtOpenDestFile = null; - - private TraceLevel m_traceLevel = TraceLevel.INPUT_AND_ERRORS; - private TraceLevel m_jtOpenTraceLevel = TraceLevel.OFF; - private Dest m_jtopenDest = Dest.IN_MEM; - - private Tracer() { - PrintWriter jt400PrintWriter = new PrintWriter(new Writer() { - @Override - public void write(char[] _cbuf, int _off, int _len) throws IOException { - String data = new String(_cbuf, _off, _len); - if (Dest.IN_MEM == m_jtopenDest) { - m_jtOpenInMem.add(data); - return; - } - if (null == m_jtOpenFileWriter) { - try { - m_jtOpenFileWriter = new PrintWriter(getJtOpenFile(), "UTF-8"); - } catch (Exception e) { - e.printStackTrace(); - m_jtopenDest = Dest.IN_MEM; - m_jtOpenInMem.add(data); - } - } - m_jtOpenFileWriter.write(data); - if (data.contains("\n")) { - m_jtOpenFileWriter.flush(); - } - } - @Override - public void flush() throws IOException { - if (null != m_jtOpenFileWriter) { - m_jtOpenFileWriter.flush(); - } - } + private TraceLevel m_traceLevel; - @Override - public void close() throws IOException { - if (null != m_jtOpenFileWriter) { - m_jtOpenFileWriter.close(); - m_jtOpenFileWriter = null; - } - } - }); - try { - Trace.setPrintWriter(jt400PrintWriter); - } catch (IOException e) { - e.printStackTrace(); - } + private final String m_connectionId; // ✅ NEW: For per-connection tracing in daemon mode + + private final boolean m_isGlobal; + + private Tracer(boolean _isGlobal) { + m_connectionId = _isGlobal ? GLOBAL_CONNECTION_ID : (""+s_connectionIdGenerator.incrementAndGet()); + m_isGlobal = _isGlobal; + m_traceLevel = _isGlobal? TraceLevel.ON :TraceLevel.INPUT_AND_ERRORS; + m_dest = _isGlobal ? Dest.DEV_NULL_OR_STDERR : Dest.IN_MEM; } - public Tracer setTraceLevel(TraceLevel _l) { - if(!MapepireServer.isSingleMode()) { - return this; - } - m_traceLevel = _l; - return this; + /** + * Get the current connection ID for this Tracer instance. + * + * @return the connection ID, or null if this is the global tracer + */ + public String getConnectionId() { + return m_connectionId; } - public Tracer setJtOpenTraceLevel(TraceLevel _l) { - if(!MapepireServer.isSingleMode()) { - return this; - } - switch (_l) { - case OFF: - Trace.setTraceOn(false); - Trace.setTraceAllOn(false); - Trace.setTraceDatastreamOn(false); - break; - case ON: - Trace.setTraceOn(true); - Trace.setTraceAllOn(true); - Trace.setTraceDatastreamOn(false); - break; - case DATASTREAM: - Trace.setTraceOn(true); - Trace.setTraceAllOn(true); - Trace.setTraceDatastreamOn(true); - break; - case ERRORS: - Trace.setTraceOn(true); - Trace.setTraceAllOn(false); - Trace.setTraceErrorOn(true); - break; - } + public Tracer setTraceLevel(TraceLevel _l) { + // ✅ FIXED: Allow tracing in daemon mode (removed single mode check) + m_traceLevel = _l; return this; } public Tracer setDest(Dest _dest) { - if(!MapepireServer.isSingleMode()) { - return this; - } + // ✅ FIXED: Allow destination changes in daemon mode (removed single mode check) if (m_dest == _dest) { return this; } @@ -298,26 +331,7 @@ public Tracer setDest(Dest _dest) { return this; } - public Tracer setJtOpenDest(Dest _dest) throws FileNotFoundException, UnsupportedEncodingException, IOException { - if(!MapepireServer.isSingleMode()) { - return this; - } - if (m_jtopenDest == _dest) { - return this; - } - if (Dest.FILE == m_dest && null != m_jtOpenFileWriter) { - m_jtOpenFileWriter.flush(); - m_jtOpenFileWriter.close(); - m_jtOpenFileWriter = null; - } - m_jtopenDest = _dest; - return this; - } - public String getDestString() throws IOException { - if(!MapepireServer.isSingleMode()) { - return "unknown"; - } switch (m_dest) { case FILE: return getFile().getAbsolutePath(); @@ -328,32 +342,11 @@ public String getDestString() throws IOException { } } - public String getJtOpenDestString() throws IOException { - if(!MapepireServer.isSingleMode()) { - return "unknown"; - } - switch (m_dest) { - case FILE: - return getJtOpenFile().getAbsolutePath(); - case IN_MEM: - return "IN_MEM"; - default: - return "unknown"; - } - } - public TraceLevel getTraceLevel() { return m_traceLevel; } - public TraceLevel getJtOpenTraceLevel() { - return m_jtOpenTraceLevel; - } - public StringBuffer getRawData() throws IOException { - if(!MapepireServer.isSingleMode()) { - return new StringBuffer(""); - } StringBuffer buf = new StringBuffer(); if (Dest.IN_MEM == m_dest) { buf.append("\n\n"); @@ -364,8 +357,7 @@ public StringBuffer getRawData() throws IOException { } } } else { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(new FileInputStream(getFile()), "UTF-8"))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(getFile()), "UTF-8"))) { String lineString = null; while (null != (lineString = reader.readLine())) { buf.append(lineString); @@ -377,31 +369,6 @@ public StringBuffer getRawData() throws IOException { return buf; } - public StringBuffer getJtOpenRawData() throws UnsupportedEncodingException, FileNotFoundException, IOException { - if(!MapepireServer.isSingleMode()) { - return new StringBuffer(""); - } - StringBuffer buf = new StringBuffer(); - if (Dest.IN_MEM == m_jtopenDest) { - synchronized (m_jtOpenInMem) { - for (String l : m_jtOpenInMem.getEntries()) { - buf.append(l); - } - } - } else { - Trace.getPrintWriter().flush(); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(new FileInputStream(getJtOpenFile()), "UTF-8"))) { - String lineString = null; - while (null != (lineString = reader.readLine())) { - buf.append(lineString); - buf.append("\r\n"); - } - } - } - return buf; - } - private Tracer Trace(EventType _t, Object _data) { // TODO: audit fallback cases with use of printStackTrace() throughout if ((_data instanceof Throwable) && !System.getProperty("os.name", "").contains("400")) { @@ -410,8 +377,26 @@ private Tracer Trace(EventType _t, Object _data) { if (!_t.isLoggedAt(m_traceLevel)) { return this; } + if(null == _data) { + return this; + } + + if(m_isGlobal) { + final String simpleData = String.format("%s: %s", _t.name(), _data.toString()); + SystemNativeUtils.writeToJobLog(s_globalTracer, simpleData); + if(m_dest == Dest.DEV_NULL_OR_STDERR) { + System.err.println(simpleData); + } + } + + if (m_dest == Dest.DEV_NULL_OR_STDERR) { + return this; + } + + Entry entry = new Entry(_t, _data); + if (Dest.IN_MEM == m_dest) { - m_inMem.add(new Entry(_t, _data)); + m_inMem.add(entry); return this; } if (null == m_fileWriter) { @@ -438,39 +423,14 @@ private Tracer Trace(EventType _t, Object _data) { return this; } - private File getFile() throws IOException { + private synchronized File getFile() throws IOException { if (null != m_destFile) { return m_destFile; } - try { - URL location = Tracer.class.getProtectionDomain().getCodeSource().getLocation(); - File f = new File(location.toURI()); - File dir = f.isDirectory() ? f : f.getParentFile(); - String dateStr = getDateFormatter().format(new Date()); - String fileName = String.format("vsc-%s-%s.html", dateStr, s_pseudoPid); - File ret = m_destFile = new File(dir, fileName); - ret.createNewFile(); - return m_destFile = ret; - } catch (Exception e) { - return m_destFile = File.createTempFile("VSCode", ".html"); - } - } - - private File getJtOpenFile() throws IOException { - if (null != m_jtOpenDestFile) { - return m_jtOpenDestFile; - } - try { - URL location = Tracer.class.getProtectionDomain().getCodeSource().getLocation(); - File f = new File(location.toURI()); - File dir = f.isDirectory() ? f : f.getParentFile(); - String dateStr = getDateFormatter().format(new Date()); - String fileName = String.format("vsc-jtopen-%s-%s.txt", dateStr, s_pseudoPid); - File ret = new File(dir, fileName); - ret.createNewFile(); - return m_jtOpenDestFile = ret; - } catch (Exception e) { - return m_jtOpenDestFile = File.createTempFile("VSCode-jtopen", ".txt"); - } + String dateStr = getDateFormatter().format(new Date()); + String filePrefix = String.format("vscode-%s-%s-", dateStr, s_pseudoPid); + File ret = m_destFile = File.createTempFile(filePrefix, ".html"); + ret.createNewFile(); + return m_destFile = ret; } } diff --git a/src/main/java/com/github/ibm/mapepire/Version.java b/src/main/java/com/github/ibm/mapepire/Version.java index 225954b..096834a 100644 --- a/src/main/java/com/github/ibm/mapepire/Version.java +++ b/src/main/java/com/github/ibm/mapepire/Version.java @@ -1,5 +1,5 @@ package com.github.ibm.mapepire; public class Version { - static public final String s_compileDateTime = "2024-08-08 00:36:20 (GMT)"; - static public final String s_version = "2.0.0-rc1"; + static public final String s_compileDateTime = "2026-08-12 16:34:46 (GMT)"; + static public final String s_version = "2.3.5"; } \ No newline at end of file diff --git a/src/main/java/com/github/ibm/mapepire/requests/GetTraceData.java b/src/main/java/com/github/ibm/mapepire/requests/GetTraceData.java deleted file mode 100644 index f56f68d..0000000 --- a/src/main/java/com/github/ibm/mapepire/requests/GetTraceData.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.ibm.mapepire.requests; - -import com.github.ibm.mapepire.ClientRequest; -import com.github.ibm.mapepire.DataStreamProcessor; -import com.github.ibm.mapepire.SystemConnection; -import com.github.ibm.mapepire.Tracer; -import com.google.gson.JsonObject; - -public class GetTraceData extends ClientRequest { - - public GetTraceData(final DataStreamProcessor _io, final SystemConnection m_conn, final JsonObject _reqObj) { - super(_io, m_conn, _reqObj); - } - - @Override - public void go() throws Exception { - StringBuffer rawData = Tracer.get().getRawData(); - StringBuffer jtOpenData = Tracer.get().getJtOpenRawData(); - addReplyData("tracedata", rawData); - addReplyData("jtopentracedata", jtOpenData); - } - @Override - public boolean isForcedSynchronous() { - return true; - } -} diff --git a/src/main/java/com/github/ibm/mapepire/ws/DbWebsocketClient.java b/src/main/java/com/github/ibm/mapepire/ws/DbWebsocketClient.java index 6dc0986..203df92 100644 --- a/src/main/java/com/github/ibm/mapepire/ws/DbWebsocketClient.java +++ b/src/main/java/com/github/ibm/mapepire/ws/DbWebsocketClient.java @@ -1,7 +1,9 @@ package com.github.ibm.mapepire.ws; +import com.github.ibm.mapepire.ConnectionTraceContext; import com.github.ibm.mapepire.DataStreamProcessor; import com.github.ibm.mapepire.SystemConnection; +import com.github.ibm.mapepire.Tracer; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketAdapter; import org.eclipse.jetty.websocket.api.WebSocketException; @@ -9,22 +11,35 @@ import java.io.*; import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; public class DbWebsocketClient extends WebSocketAdapter { + private final CountDownLatch closureLatch = new CountDownLatch(1); private final DataStreamProcessor io; + private final Tracer tracer; DbWebsocketClient(String clientHost, String clientAddress, String host, String user, String pass) throws IOException { super(); - SystemConnection conn = new SystemConnection(clientHost, clientAddress,host, user, pass); + // Generate unique connection ID using AtomicLong for better performance + this.connectionId = String.valueOf(connectionIdGenerator.incrementAndGet()); + + // Create per-connection tracer instance + this.tracer = Tracer.getNew(connectionId); + + SystemConnection conn = new SystemConnection(clientHost, clientAddress, host, user, pass, tracer); io = getDataStream(this, conn); + + // Log connection establishment + tracer.logInfo("WebSocket connection established: " + connectionId + + " (Client: " + clientHost + ", User: " + user + ")"); } @Override public void onWebSocketConnect(Session sess) { super.onWebSocketConnect(sess); sess.setIdleTimeout(Integer.MAX_VALUE); - System.out.println("Socket Connected: " + sess); + tracer.logInfo("Socket Connected: " + sess + " [Connection ID: " + connectionId + "]"); } @Override @@ -37,6 +52,13 @@ public void onWebSocketText(String message) { public void onWebSocketClose(int statusCode, String reason) { io.end(); super.onWebSocketClose(statusCode, reason); + + // Log connection closure + tracer.logInfo("WebSocket connection closed: " + connectionId + + " (Status: " + statusCode + ", Reason: " + reason + ")"); + + // Cleanup per-connection trace context + ConnectionTraceContext.remove(connectionId); closureLatch.countDown(); } @@ -44,7 +66,9 @@ public void onWebSocketClose(int statusCode, String reason) { public void onWebSocketError(Throwable cause) { io.end(); super.onWebSocketError(cause); - // cause.printStackTrace(System.err); + // Log error to per-connection trace + tracer.logErr("WebSocket error on connection " + connectionId + ": " + cause.getMessage()); + tracer.logErr(cause); } public void awaitClosure() throws InterruptedException { @@ -74,7 +98,7 @@ public synchronized void flush() throws IOException { try { endpoint.getRemote().sendString(message); } catch (WebSocketException e){ - System.err.println("Could not send message due to error: " + e.getMessage()); + endpoint.tracer.logErr("Could not send message on connection " + endpoint.connectionId + ": " + e.getMessage()); } } } @@ -86,4 +110,4 @@ public synchronized void flush() throws IOException { return new DataStreamProcessor(in, out, conn, false); } -} +} \ No newline at end of file