• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 26982556974

04 Jun 2026 10:08PM UTC coverage: 63.677% (+0.3%) from 63.427%
26982556974

push

github

ibacher
TRUNK-6392: Improvements to the OpenMRS Logging plugin (#6104)

151 of 195 new or added lines in 8 files covered. (77.44%)

12 existing lines in 5 files now uncovered.

23830 of 37423 relevant lines covered (63.68%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.55
/api/src/main/java/org/openmrs/logging/OpenmrsLoggingUtil.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.logging;
11

12
import java.nio.file.Paths;
13
import java.util.Locale;
14

15
import org.apache.commons.lang3.StringUtils;
16
import org.apache.logging.log4j.Level;
17
import org.apache.logging.log4j.LogManager;
18
import org.apache.logging.log4j.core.Appender;
19
import org.apache.logging.log4j.core.Logger;
20
import org.apache.logging.log4j.core.LoggerContext;
21
import org.apache.logging.log4j.core.appender.AbstractFileAppender;
22
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
23
import org.apache.logging.log4j.core.appender.FileAppender;
24
import org.apache.logging.log4j.core.appender.MemoryMappedFileAppender;
25
import org.apache.logging.log4j.core.appender.RandomAccessFileAppender;
26
import org.apache.logging.log4j.core.appender.RollingFileAppender;
27
import org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender;
28
import org.apache.logging.log4j.core.config.LoggerConfig;
29
import org.apache.logging.log4j.status.StatusLogger;
30
import org.jspecify.annotations.NonNull;
31
import org.jspecify.annotations.Nullable;
32
import org.openmrs.annotation.Logging;
33
import org.openmrs.api.ServiceNotFoundException;
34
import org.openmrs.api.context.Context;
35
import org.openmrs.util.ConfigUtil;
36
import org.openmrs.util.OpenmrsConstants;
37
import org.openmrs.util.PrivilegeConstants;
38
import org.slf4j.LoggerFactory;
39

40
/**
41
 * Utility methods related to logging.
42
 * In general, module-level code should likely only call {@link #getMemoryAppender()} and
43
 * {@link #getOpenmrsLogLocation()}, however, the methods to update loggers are also exposed if necessary.
44
 *
45
 * @since 2.4.4, 2.5.1, 2.6.0
46
 */
47
public final class OpenmrsLoggingUtil {
48
        
49
        private static final org.slf4j.Logger log = LoggerFactory.getLogger(OpenmrsLoggingUtil.class);
1✔
50
        
51
        private OpenmrsLoggingUtil() {
52
        }
53
        
54
        /**
55
         * Gets the in-memory log appender. This method needed to be added as it is much more difficult to
56
         * get a specific appender in the Log4J2 architecture. This method is called in places where we need
57
         * to display logging message.
58
         *
59
         * @since 2.4.4, 2.5.1, 2.6.0
60
         */
61
        @Logging(ignore = true)
62
        public static MemoryAppender getMemoryAppender() {
NEW
63
                LoggerContext context = getLog4j2Context();
×
NEW
64
                if (context == null) {
×
NEW
65
                        return null;
×
66
                }
67

NEW
68
                MemoryAppender memoryAppender = context.getConfiguration().getAppender(OpenmrsConstants.MEMORY_APPENDER_NAME);
×
69

70
                if (memoryAppender != null && !memoryAppender.isStarted()) {
×
71
                        memoryAppender.start();
×
72
                }
73
                
74
                return memoryAppender;
×
75
        }
76
        
77
        /**
78
         * Returns the location of the OpenMRS log file.
79
         * <p/>
80
         * <strong>Warning:</strong> the result of this call can return null if either the file appender uses a name other than
81
         * {@link OpenmrsConstants#LOG_OPENMRS_FILE_APPENDER} or if the appender with that name is not one of the default log4j2
82
         * file appending types.
83
         *
84
         * @return the path to the OpenMRS log file
85
         * */
86
        public static String getOpenmrsLogLocation() {
87
                LoggerContext context = getLog4j2Context();
1✔
88
                if (context == null) {
1✔
NEW
89
                        return null;
×
90
                }
91

92
                Appender fileAppender = context.getConfiguration().getAppender(OpenmrsConstants.LOG_OPENMRS_FILE_APPENDER);
1✔
93

94
                String fileName = null;
1✔
95
                if (fileAppender instanceof AbstractOutputStreamAppender) {
1✔
96
                        if (fileAppender instanceof RollingFileAppender) {
×
97
                                fileName = ((RollingFileAppender) fileAppender).getFileName();
×
98
                        } else if (fileAppender instanceof FileAppender) {
×
99
                                fileName = ((FileAppender) fileAppender).getFileName();
×
100
                        } else if (fileAppender instanceof MemoryMappedFileAppender) {
×
101
                                fileName = ((MemoryMappedFileAppender) fileAppender).getFileName();
×
102
                        } else if (fileAppender instanceof RollingRandomAccessFileAppender) {
×
103
                                fileName = ((RollingRandomAccessFileAppender) fileAppender).getFileName();
×
104
                        } else if (fileAppender instanceof RandomAccessFileAppender) {
×
105
                                fileName = ((RandomAccessFileAppender) fileAppender).getFileName();
×
106
                        } else if (fileAppender instanceof AbstractFileAppender) {
×
107
                                fileName = ((AbstractFileAppender<?>) fileAppender).getFileName();
×
108
                        } else {
109
                                return null;
×
110
                        }
111
                }
112
                
113
                return fileName == null ? null : Paths.get("", fileName).toAbsolutePath().toString();
1✔
114
        }
115
        
116
        /**
117
         * Sets the org.openmrs Log4J logger's level if global property log.level.openmrs (
118
         * OpenmrsConstants.GLOBAL_PROPERTY_LOG_LEVEL ) exists. Valid values for global property are
119
         * trace, debug, info, warn, error or fatal.
120
         */
121
        @Logging(ignore = true)
122
        public static void applyLogLevels() {
123
                applyLogLevels(null);
1✔
124
        }
1✔
125

126
        @Logging(ignore = true)
127
        public static void applyLogLevels(String logLevelGp) {
128
                // Check system and runtime properties first — these do not require a session
129
                String logLevel = ConfigUtil.getSystemProperty(OpenmrsConstants.GLOBAL_PROPERTY_LOG_LEVEL);
1✔
130
                if (logLevel == null) {
1✔
131
                        logLevel = ConfigUtil.getRuntimeProperty(OpenmrsConstants.GLOBAL_PROPERTY_LOG_LEVEL);
1✔
132
                }
133

134
                if (logLevel == null && logLevelGp != null) {
1✔
135
                        logLevel = logLevelGp;
1✔
136
                        // Fall back to global property only if a session is open
137
                } else if (logLevel != null && logLevelGp != null) {
1✔
NEW
138
                        StatusLogger.getLogger().info("Ignoring GP value \"{}\" as a system or runtime property is already set",
×
139
                            logLevelGp);
140
                } else if (logLevel == null && Context.isSessionOpen()) {
1✔
141
                        Context.addProxyPrivilege(PrivilegeConstants.GET_GLOBAL_PROPERTIES);
1✔
142
                        try {
143
                                logLevel = ConfigUtil.getGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_LOG_LEVEL);
1✔
NEW
144
                        } catch (ServiceNotFoundException e) {
×
NEW
145
                                StatusLogger.getLogger().error("An exception was thrown while trying to get the {} global property",
×
146
                                    OpenmrsConstants.GLOBAL_PROPERTY_LOG_LEVEL, e);
NEW
147
                                return;
×
148
                        } finally {
149
                                Context.removeProxyPrivilege(PrivilegeConstants.GET_GLOBAL_PROPERTIES);
1✔
150
                        }
151
                }
152

153
                if (logLevel == null) {
1✔
154
                        return;
1✔
155
                }
156

157
                synchronized (OpenmrsLoggingUtil.class) {
1✔
158
                        for (String level : logLevel.split(",")) {
1✔
159
                                String[] classAndLevel = level.split(":", 3);
1✔
160
                                if (classAndLevel.length == 0) {
1✔
161
                                        break;
×
162
                                } else if (classAndLevel.length == 1) {
1✔
163
                                        applyLogLevelInternal(OpenmrsConstants.LOG_CLASS_DEFAULT, classAndLevel[0].trim());
1✔
164
                                } else {
165
                                        if (classAndLevel.length > 2) {
1✔
NEW
166
                                                StatusLogger.getLogger().warn(
×
167
                                                    "Could not properly parse \"{}\" into a class and level due to too many colons. Expected format is <class>:<level>, e.g., org.openmrs.api:INFO",
168
                                                    level);
169
                                        }
170
                                        applyLogLevelInternal(classAndLevel[0].trim(), classAndLevel[1].trim());
1✔
171
                                }
172
                        }
173
                        
174
                        // DO NOT USE LogManager#getContext() here as this might reset the logger context
175
                        LoggerContext context = getLog4j2Context();
1✔
176
                        if (context != null) {
1✔
177
                                context.updateLoggers();
1✔
178
                        }
179
                }
1✔
180
        }
1✔
181
        
182
        /**
183
         * Set the log4j log level for class <code>logClass</code> to <code>logLevel</code>.
184
         *
185
         * @param logClass optional string giving the class level to change. Defaults to
186
         *                 {@link OpenmrsConstants#LOG_CLASS_DEFAULT} . Should be something like org.openmrs.___
187
         * @param logLevel one of <tt>OpenmrsConstants.LOG_LEVEL_*</tt> constants
188
         */
189
        public static void applyLogLevel(String logClass, String logLevel) {
190
                if (StringUtils.isNotBlank(logLevel)) {
1✔
191
                        synchronized (OpenmrsLoggingUtil.class) {
1✔
192
                                applyLogLevelInternal(logClass, logLevel);
1✔
193

194
                                LoggerContext context = getLog4j2Context();
1✔
195
                                if (context != null) {
1✔
196
                                        context.updateLoggers();
1✔
197
                                }
198
                        }
1✔
199
                }
200
        }
1✔
201
        
202
        /**
203
         * This method is the implementation of applying a level to a logger. It is intended to be called in an
204
         * already synchronized context. Note these changes will only be applied once a call to
205
         * {@link LoggerContext#updateLoggers()} is made.
206
         *
207
         * @param logClass optional string giving the class level to change. Defaults to
208
         *                 *            OpenmrsConstants.LOG_CLASS_DEFAULT . Should be something like org.openmrs.___
209
         * @param logLevel one of OpenmrsConstants.LOG_LEVEL_* constants
210
         */
211
        private static void applyLogLevelInternal(String logClass, String logLevel) {
212
                if (StringUtils.isNotBlank(logLevel)) {
1✔
213
                        // the default log class is org.openmrs.api
214
                        if (StringUtils.isEmpty(logClass)) {
1✔
215
                                logClass = OpenmrsConstants.LOG_CLASS_DEFAULT;
1✔
216
                        }
217
                        
218
                        // DO NOT USE LogManager#getContext() here as this will reset the logger context
219
                        LoggerContext context = getLog4j2Context();
1✔
220
                        if (context == null) {
1✔
NEW
221
                                return;
×
222
                        }
223

224
                        LoggerConfig configuration = context.getConfiguration().getLoggerConfig(logClass);
1✔
225
                        Level level = stringToLevel(logLevel, logClass);
1✔
226

227
                        if (configuration == null || !configuration.getName().equals(logClass)) {
1✔
228
                                configuration = new LoggerConfig(logClass, level, true);
1✔
229
                                context.getConfiguration().addLogger(logClass, configuration);
1✔
230
                        } else {
231
                                configuration.setLevel(level);
1✔
232
                        }
233
                }
234
        }
1✔
235
        
236
        /**
237
         * Reloads the logging configuration
238
         */
239
        public static void reloadLoggingConfiguration() {
240
                org.apache.logging.log4j.spi.LoggerContext context = LogManager.getContext(true);
1✔
241
                if (context instanceof LoggerContext) {
1✔
242
                        // The general interface does not guarantee that reconfigure() exists
243
                        ((LoggerContext) context).reconfigure();
1✔
244
                } else {
NEW
245
                        StatusLogger.getLogger()
×
NEW
246
                                .warn("Unable to reload logging configuration as we are not using the Log4J2 LoggerContext");
×
247
                }
248
        }
1✔
249

250
        /**
251
         * Takes a string representing a log level and returns the corresponding {@code Level} object.
252
         * <p/>
253
         * Used to support user-supplied log-levels and backwards compatibility with the Log4J1 API.
254
         *
255
         * @param logLevel The string level to convert
256
         * @return The corresponding {@code Level} or {@code Level#WARN} if unknown
257
         */
258
        public static Level stringToLevel(@NonNull String logLevel) {
259
                return stringToLevel(logLevel, null);
1✔
260
        }
261

262
        /**
263
         * Takes a string representing a log level and returns the corresponding {@code Level} object.
264
         * <p/>
265
         * Used to support user-supplied log-levels and backwards compatibility with the Log4J1 API.
266
         *
267
         * @param logLevel The string level to convert
268
         * @param logClass The class for this logger. Used to vary the default for the default logger.
269
         * @return The corresponding {@code Level} or {@code Level#WARN} if unknown
270
         * @see OpenmrsConstants#LOG_CLASS_DEFAULT
271
         */
272
        public static Level stringToLevel(@NonNull String logLevel, @Nullable String logClass) {
273
                Level level;
274
                if (logLevel == null) {
1✔
275
                        return Level.WARN;
1✔
276
                }
277

278
                logLevel = logLevel.toLowerCase(Locale.ROOT);
1✔
279
                switch (logLevel) {
1✔
280
                        case OpenmrsConstants.LOG_LEVEL_TRACE:
281
                                level = Level.TRACE;
1✔
282
                                break;
1✔
283
                        case OpenmrsConstants.LOG_LEVEL_DEBUG:
284
                                level = Level.DEBUG;
1✔
285
                                break;
1✔
286
                        case OpenmrsConstants.LOG_LEVEL_INFO:
287
                                level = Level.INFO;
1✔
288
                                break;
1✔
289
                        case OpenmrsConstants.LOG_LEVEL_WARN:
290
                                level = Level.WARN;
1✔
291
                                break;
1✔
292
                        case OpenmrsConstants.LOG_LEVEL_ERROR:
293
                                level = Level.ERROR;
1✔
294
                                break;
1✔
295
                        case OpenmrsConstants.LOG_LEVEL_FATAL:
296
                                level = Level.FATAL;
1✔
297
                                break;
1✔
298
                        default:
299
                                log.warn("Log level {} is invalid. " + "Valid values are trace, debug, info, warn, error or fatal",
1✔
300
                                    logLevel);
301
                                if (logClass != null && logClass.equals(OpenmrsConstants.LOG_CLASS_DEFAULT)) {
1✔
302
                                        level = Level.INFO;
1✔
303
                                } else {
304
                                        level = Level.WARN;
1✔
305
                                }
306
                                break;
307
                }
308

309
                return level;
1✔
310
        }
311

312
        private static LoggerContext getLog4j2Context() {
313
                // DO NOT USE LogManager#getContext() here as this will reset the logger context
314
                org.apache.logging.log4j.Logger rootLogger = LogManager.getRootLogger();
1✔
315
                if (!(rootLogger instanceof Logger)) {
1✔
NEW
316
                        StatusLogger.getLogger()
×
NEW
317
                                .error("Could not get the LoggerContext as this configuration is not using the standard Log4J2 context");
×
NEW
318
                        return null;
×
319
                }
320

321
                return ((Logger) rootLogger).getContext();
1✔
322
        }
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc