Skip to content

Commit 75a026d

Browse files
committed
standardize python logger with JUL
1 parent 00d4077 commit 75a026d

4 files changed

Lines changed: 129 additions & 15 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package usace.rowcps.headless;
2+
3+
import java.util.logging.ErrorManager;
4+
import java.util.logging.Formatter;
5+
import java.util.logging.Handler;
6+
import java.util.logging.LogRecord;
7+
8+
public final class PythonJulHandler extends Handler {
9+
10+
private final PythonLogSink sink;
11+
12+
private final Formatter messageFormatter = new Formatter() {
13+
@Override
14+
public String format(LogRecord record) {
15+
return formatMessage(record);
16+
}
17+
};
18+
19+
public PythonJulHandler(PythonLogSink sink) {
20+
this.sink = sink;
21+
}
22+
23+
@Override
24+
public void publish(LogRecord record) {
25+
if (record == null || !isLoggable(record)) {
26+
return;
27+
}
28+
29+
try {
30+
String message = messageFormatter.format(record);
31+
sink.log(record, message);
32+
} catch (RuntimeException ex) {
33+
// Safeguard against failures in Python log handling.
34+
reportError("Failed to publish JUL record to Python logging.", ex, ErrorManager.WRITE_FAILURE);
35+
}
36+
}
37+
38+
@Override
39+
public void flush() {
40+
// No-op. Python logging handlers manage their own flushing.
41+
}
42+
43+
@Override
44+
public void close() {
45+
// No-op.
46+
}
47+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package usace.rowcps.headless;
2+
3+
import java.util.logging.LogRecord;
4+
5+
public interface PythonLogSink {
6+
void log(LogRecord record, String message);
7+
}

regi-headless/src/main/python/regi_python/regi_python.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
# Copyright (c) 2026
2-
# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC)
3-
# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL.
4-
# Source may not be released without written approval from HEC
5-
61
import os
7-
import logging
82
import jpype
93
import jpype.imports
104
from contextlib import contextmanager
11-
from pathlib import Path
12-
from .regi_python_logging import configure_logging
5+
from .regi_python_logging import configure_logging, configure_jul_to_python_logging
136

147
logger = configure_logging()
158

@@ -35,6 +28,7 @@ def regi_session():
3528
convertStrings=True,
3629
classpath=[LIB_PATH]
3730
)
31+
configure_jul_to_python_logging(logger)
3832

3933
try:
4034
yield

regi-headless/src/main/python/regi_python/regi_python_logging.py

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
# Copyright (c) 2026
2-
# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC)
3-
# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL.
4-
# Source may not be released without written approval from HEC
5-
6-
import logging
71
import os
2+
import logging
3+
from jpype import JImplements, JOverride
4+
5+
_java_log_sink = None
86

97

108
def _get_log_level():
@@ -15,6 +13,7 @@ def _get_log_level():
1513
log_level = logging.INFO
1614
return log_level, invalid_log_level, log_level_name
1715

16+
1817
def configure_logging():
1918
log_level, invalid_log_level, log_level_name = _get_log_level()
2019

@@ -44,4 +43,71 @@ def filter(self, record):
4443
if invalid_log_level:
4544
logger.warning("Invalid REGI_LOG_LEVEL '%s'; using INFO.", log_level_name)
4645

47-
return logger
46+
return logger
47+
48+
49+
def configure_jul_to_python_logging(python_logger):
50+
global _java_log_sink
51+
52+
from java.util.logging import Logger, Level
53+
from usace.rowcps.headless import PythonJulHandler
54+
55+
java_level = _python_level_to_jul_level(python_logger.getEffectiveLevel())
56+
57+
root_logger = Logger.getLogger("")
58+
root_logger.setLevel(java_level)
59+
60+
for handler in root_logger.getHandlers():
61+
root_logger.removeHandler(handler)
62+
63+
_java_log_sink = PythonLogSink(python_logger)
64+
65+
handler = PythonJulHandler(_java_log_sink)
66+
handler.setLevel(java_level)
67+
68+
root_logger.addHandler(handler)
69+
70+
71+
def _python_level_to_jul_level(python_level):
72+
from java.util.logging import Level
73+
74+
if python_level <= logging.DEBUG:
75+
return Level.FINE
76+
if python_level <= logging.INFO:
77+
return Level.INFO
78+
if python_level <= logging.WARNING:
79+
return Level.WARNING
80+
if python_level <= logging.ERROR:
81+
return Level.SEVERE
82+
return Level.OFF
83+
84+
85+
def _jul_level_to_python_level(jul_level_name):
86+
mapping = {
87+
"SEVERE": logging.ERROR,
88+
"WARNING": logging.WARNING,
89+
"INFO": logging.INFO,
90+
"CONFIG": logging.INFO,
91+
"FINE": logging.DEBUG,
92+
"FINER": logging.DEBUG,
93+
"FINEST": logging.DEBUG,
94+
}
95+
return mapping.get(str(jul_level_name), logging.INFO)
96+
97+
98+
@JImplements("usace.rowcps.headless.PythonLogSink")
99+
class PythonLogSink:
100+
def __init__(self, python_logger):
101+
self._logger = python_logger
102+
103+
@JOverride
104+
def log(self, record, message):
105+
level = _jul_level_to_python_level(record.getLevel().getName())
106+
name = str(record.getLoggerName() or "java")
107+
message = str(message)
108+
109+
thrown = record.getThrown()
110+
if thrown is not None:
111+
message = f"{message}\n{thrown}"
112+
113+
self._logger.log(level, "[%s] %s", name, message)

0 commit comments

Comments
 (0)