• 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

88.06
/api/src/main/java/org/openmrs/logging/MemoryAppender.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.io.Serializable;
13
import java.lang.ref.SoftReference;
14
import java.util.Arrays;
15
import java.util.Collections;
16
import java.util.HashMap;
17
import java.util.List;
18
import java.util.Map;
19
import java.util.Objects;
20
import java.util.concurrent.atomic.AtomicReference;
21
import java.util.stream.Collectors;
22

23
import org.apache.logging.log4j.core.Appender;
24
import org.apache.logging.log4j.core.Core;
25
import org.apache.logging.log4j.core.Filter;
26
import org.apache.logging.log4j.core.Layout;
27
import org.apache.logging.log4j.core.LogEvent;
28
import org.apache.logging.log4j.core.StringLayout;
29
import org.apache.logging.log4j.core.appender.AbstractAppender;
30
import org.apache.logging.log4j.core.config.Property;
31
import org.apache.logging.log4j.core.config.plugins.Plugin;
32
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
33
import org.apache.logging.log4j.core.config.plugins.PluginElement;
34
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
35
import org.apache.logging.log4j.core.layout.PatternLayout;
36
import org.apache.logging.log4j.status.StatusLogger;
37
import org.openmrs.util.OpenmrsConstants;
38
import org.openmrs.util.ThreadSafeCircularFifoQueue;
39

40
/**
41
 * This class stores a configurable number lines of the output from the log file.
42
 * <p/>
43
 * Note that this class is implemented as a single-buffer-per-appender-name meaning that each
44
 * appender name can only support a single buffer size (the most recently applied)
45
 */
46
@Plugin(name = "Memory", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
47
public class MemoryAppender extends AbstractAppender {
48

49
        // we store the buffers by name, using SoftReferences to allow them to be garbage collected
50
        // this is a HashMap as it is only accessed from a synchronized method
51
        private static final Map<String, SoftReference<AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>>>> BUFFERS = new HashMap<>(
1✔
52
                1);
53

54
        private final AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>> bufferRef;
55

56
        private final int bufferSize;
57

58
        protected MemoryAppender(String name, Filter filter, StringLayout layout, boolean ignoreExceptions,
59
            Property[] properties, AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>> bufferRef) {
60
                super(name, filter, layout, ignoreExceptions, properties);
1✔
61

62
                this.bufferRef = bufferRef;
1✔
63
                this.bufferSize = bufferRef.get().capacity();
1✔
64
        }
1✔
65

66
        protected MemoryAppender(String name, Filter filter,
67
                StringLayout layout, boolean ignoreExceptions,
68
                Property[] properties, int bufferSize) {
UNCOV
69
                super(name, filter, layout, ignoreExceptions, properties);
×
70

NEW
71
                this.bufferRef = getBufferRef(name, bufferSize);
×
NEW
72
                this.bufferSize = bufferRef.get().capacity();
×
UNCOV
73
        }
×
74

75
        public static MemoryAppenderBuilder newBuilder() {
76
                return new MemoryAppenderBuilder();
1✔
77
        }
78

79
        @PluginFactory
80
        @SuppressWarnings("unused")
81
        protected static MemoryAppender createAppender(
82
                @PluginAttribute("name") final String name,
83
                @PluginAttribute("bufferSize") final int bufferSize,
84
                @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) final boolean ignoreExceptions,
85
                @PluginElement("Filter") final Filter filter,
86
                @PluginElement("Layout") final StringLayout layout
87
        ) {
88
                final int theBufferSize = bufferSize <= 0 ? 100 : bufferSize;
1✔
89
                AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>> buffer = getBufferRef(name, theBufferSize);
1✔
90

91
                MemoryAppender appender = new MemoryAppender(name, filter, layout, ignoreExceptions, null, buffer);
1✔
92
                if (!appender.isStarted()) {
1✔
93
                        appender.start();
1✔
94
                }
95
                return appender;
1✔
96
        }
97

98
        @Override
99
        public void append(LogEvent logEvent) {
100
                bufferRef.get().add(logEvent.toImmutable());
1✔
101
        }
1✔
102
        
103
        public int getBufferSize() {
104
                return bufferSize;
1✔
105
        }
106
        
107
        public List<String> getLogLines() {
108
                LogEvent[] events = bufferRef.get().toArray(new LogEvent[0]);
1✔
109
                if (events.length == 0) {
1✔
110
                        return Collections.emptyList();
1✔
111
                }
112

113
                Layout<? extends Serializable> layout = getLayout();
1✔
114
                if (!(layout instanceof StringLayout)) {
1✔
NEW
115
                        StatusLogger.getLogger().warn(
×
116
                            "MemoryAppender {} is not configured with a StringLayout; defaulting to standard OpenMRS pattern", this);
NEW
117
                        layout = PatternLayout.newBuilder().withPattern(OpenmrsConstants.DEFAULT_LOG_LAYOUT_PATTERN).build();
×
118
                }
119

120
                return Arrays.stream(events).filter(Objects::nonNull).map(((StringLayout) layout)::toSerializable)
1✔
121
                        .collect(Collectors.toList());
1✔
122
        }
123

124
        public static class MemoryAppenderBuilder extends AbstractAppender.Builder<MemoryAppenderBuilder> {
125

126
                private int bufferSize = 100;
1✔
127

128
                private StringLayout layout = PatternLayout.newBuilder().withPattern(OpenmrsConstants.DEFAULT_LOG_LAYOUT_PATTERN)
1✔
129
                        .build();
1✔
130

131
                public MemoryAppenderBuilder() {
132
                        super();
1✔
133
                        setName(OpenmrsConstants.MEMORY_APPENDER_NAME);
1✔
134
                }
1✔
135
                
136
                public MemoryAppenderBuilder setBufferSize(int bufferSize) {
137
                        if (bufferSize < 0) {
1✔
138
                                throw new IllegalArgumentException("bufferSize must be a positive number or 0");
1✔
139
                        }
140

141
                        this.bufferSize = bufferSize == 0 ? 100 : bufferSize;
1✔
142
                        return asBuilder();
1✔
143
                }
144

145
                @Override
146
                public Layout<? extends Serializable> getLayout() {
147
                        return layout;
×
148
                }
149

150
                @Override
151
                public MemoryAppenderBuilder setLayout(Layout<? extends Serializable> layout) {
152
                        if (layout instanceof StringLayout) {
1✔
153
                                return setLayout((StringLayout) layout);
×
154
                        }
155

156
                        throw new IllegalArgumentException("MemoryAppender layouts must output string values");
1✔
157
                }
158

159
                public MemoryAppenderBuilder setLayout(StringLayout layout) {
160
                        this.layout = layout;
1✔
161
                        return asBuilder();
1✔
162
                }
163

164
                public MemoryAppender build() {
165
                        String name = getName();
1✔
166
                        AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>> buffer = getBufferRef(name, bufferSize);
1✔
167
                        return new MemoryAppender(name, getFilter(), layout, isIgnoreExceptions(), getPropertyArray(), buffer);
1✔
168
                }
169
        }
170

171
        private static synchronized AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>> getBufferRef(String name,
172
                int bufferSize) {
173
                // clean-up
174
                BUFFERS.values().removeIf(ref -> ref.get() == null);
1✔
175

176
                AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>> bufferRef = null;
1✔
177
                SoftReference<AtomicReference<ThreadSafeCircularFifoQueue<LogEvent>>> ref = BUFFERS.get(name);
1✔
178
                if (ref != null) {
1✔
179
                        bufferRef = ref.get();
1✔
180
                }
181

182
                if (bufferRef == null) {
1✔
183
                        ThreadSafeCircularFifoQueue<LogEvent> queue = new ThreadSafeCircularFifoQueue<>(bufferSize);
1✔
184
                        bufferRef = new AtomicReference<>(queue);
1✔
185
                        BUFFERS.put(name, new SoftReference<>(bufferRef));
1✔
186
                } else {
1✔
187
                        ThreadSafeCircularFifoQueue<LogEvent> queue = bufferRef.get();
1✔
188
                        if (queue.capacity() != bufferSize) {
1✔
189
                                ThreadSafeCircularFifoQueue<LogEvent> resized = new ThreadSafeCircularFifoQueue<>(bufferSize);
1✔
190
                                LogEvent[] snapshot = queue.toArray(new LogEvent[0]);
1✔
191
                                // retain up to the bufferSize most recent messages when shrinking, since this is a FIFO queue
192
                                int messagesToMove = Math.min(snapshot.length, bufferSize);
1✔
193
                                int start = Math.max(0, snapshot.length - messagesToMove);
1✔
194
                                for (int i = start; i < snapshot.length; i++) {
1✔
195
                                        resized.add(snapshot[i]);
1✔
196
                                }
197
                                bufferRef.set(resized);
1✔
198
                        }
199
                }
200

201
                return bufferRef;
1✔
202
        }
203

204
}
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