|
14 | 14 | See the License for the specific language governing permissions and
|
15 | 15 | limitations under the License.
|
16 | 16 | ////
|
17 |
| -= Event Logging |
18 |
| -
|
19 |
| -The `EventLogger` class provides a simple mechanism for logging events |
20 |
| -that occur in an application. While the `EventLogger` is useful as a way |
21 |
| -of initiating events that should be processed by an audit Logging |
22 |
| -system, by itself it does not implement any of the features an audit |
23 |
| -logging system would require such as guaranteed delivery. |
24 |
| -
|
25 |
| -The recommended way of using the `EventLogger` in a typical web |
26 |
| -application is to populate the `ThreadContext` Map with data that is |
27 |
| -related to the entire lifespan of the request such as the user's id, the |
28 |
| -user's IP address, the product name, etc. This can easily be done in a |
29 |
| -servlet filter where the `ThreadContext` Map can also be cleared at the |
30 |
| -end of the request. When an event that needs to be recorded occurs a |
31 |
| -`StructuredDataMessage` should be created and populated. Then call |
32 |
| -`EventLogger.logEvent(msg)` where msg is a reference to the |
33 |
| -`StructuredDataMessage`. |
34 |
| -
|
35 |
| -[source,java] |
| 17 | += Event Logging with Log4j |
| 18 | +
|
| 19 | +The `EventLogger` class provides a mechanism for logging significant events |
| 20 | +in an application using structured data. This approach is beneficial for |
| 21 | +tracking user actions, monitoring system behavior, and debugging issues. |
| 22 | +
|
| 23 | +Theoretically, every `Logger` can be used to perform this kind of action; |
| 24 | +however, the `EventLogger` class provides a shortcut to log events with structured data |
| 25 | +since it allows for a static method to log events. |
| 26 | +
|
| 27 | +== Advantages of Structured Logging |
| 28 | +
|
| 29 | +Structured logging means events Log4j records events with detailed and structured information. |
| 30 | +That way, logs are easier to read and process. They provide better context and |
| 31 | +are also more consistent than plain text logs. |
| 32 | +
|
| 33 | +== Integration with Syslog |
| 34 | +
|
| 35 | +Log4j complies with Syslogs RFC5424 standard. |
| 36 | +This feature allows for easy integration with existing log management and monitoring systems. |
| 37 | +
|
| 38 | +== Example Configuration |
| 39 | +
|
| 40 | +To configure Log4j to output logs in Syslog (RFC5424) format, one needs to use the link:../javadoc/log4j-core/org/apache/logging/log4j/core/layout/StructuredDataLayout.html[`StructuredDataLayout`] layout. |
| 41 | +Developers can use the following configuration to log events to a local Syslog server: |
| 42 | +
|
| 43 | +[source, xml] |
| 44 | +---- |
| 45 | +<Appenders> |
| 46 | + <Syslog name="Syslog" host="localhost" port="514"> <1> |
| 47 | + <StructuredDataLayout complete="true" /> <2> |
| 48 | + </Syslog> |
| 49 | +</Appenders> |
| 50 | +
|
| 51 | +<Loggers> |
| 52 | + <Logger name="MyApp" level="info" additivity="false"> |
| 53 | + <AppenderRef ref="Syslog"/> |
| 54 | + </Logger> |
| 55 | +</Loggers> |
| 56 | +---- |
| 57 | +<1> The `Syslog` appender sends logs to a local Syslog server. |
| 58 | +<2> The `StructuredDataLayout` layout formats logs in RFC5424 format. |
| 59 | +
|
| 60 | +Of course, sending logs to a Syslog server is unnecessary. |
| 61 | +Developers can use the `StructuredDataLayout` layout with any other appender, such as `FileAppender` or `ConsoleAppender`. |
| 62 | +
|
| 63 | +As an example, the output could look like this: |
| 64 | +
|
| 65 | +[source, text] |
| 66 | +---- |
| 67 | +<165>1 2024-05-16T12:34:56.789Z myapp MyApp - ID47 [transfer@12345 toAccount="123456" fromAccount="654321" amount="1000"] User 654321 has transferred 1000 to account 123456 |
| 68 | +---- |
| 69 | +
|
| 70 | +== Using the `EventLogger` |
| 71 | +
|
| 72 | +The `EventLogger` class provides a simple way to log structured events. |
| 73 | +It uses the `StructuredDataMessage` class to create structured log messages. |
| 74 | +
|
| 75 | +Assume a simple application that performs funds transfers between accounts. |
| 76 | +This application should send a certain amount of funds from one account to another and log the event. |
| 77 | +
|
| 78 | +The account class is defined as follows, with a unique ID and a balance: |
| 79 | +
|
| 80 | +[source, java] |
| 81 | +---- |
| 82 | +class Account { |
| 83 | + private String id; |
| 84 | + private long balance; |
| 85 | + // Getters and setters omitted for brevity |
| 86 | +} |
| 87 | +---- |
| 88 | +
|
| 89 | +The `transfer()` method transfers funds between two accounts and logs the event. |
| 90 | +It needs to take two accounts and the amount to transfer as parameters. |
| 91 | +
|
| 92 | +Apart from the key-value pairs provided in the map of the `StructuredDataMessage,` |
| 93 | +the `StructuredDataMessage` also takes a unique ID, a free-form message, and a type as parameters. |
| 94 | +
|
| 95 | +The free-form message is a human-readable description of the event. |
| 96 | +This message is good for operators who need to understand the event quickly, |
| 97 | +but not so much for automated processing. |
| 98 | +
|
| 99 | +[source, java] |
| 100 | +---- |
| 101 | +public static String transferFunds(Account toAccount, Account fromAccount, long amount) { |
| 102 | + toAccount.deposit(amount); |
| 103 | + fromAccount.withdraw(amount); |
| 104 | +
|
| 105 | + // Create a unique ID for the transaction |
| 106 | + String confirm = UUID.randomUUID().toString(); |
| 107 | +
|
| 108 | + String freeFormMessage = "User " + fromAccount.getId() + " has transferred " + amount + " to account " + toAccount.getId(); <1> |
| 109 | +
|
| 110 | + // Create the StructuredDataMessage |
| 111 | + StructuredDataMessage msg = new StructuredDataMessage(confirm, freeFormMessage, "transfer"); <2> |
| 112 | + msg.put("toAccount", toAccount.getId()); <3> |
| 113 | + msg.put("fromAccount", fromAccount.getId()); |
| 114 | + msg.put("amount", amount); |
| 115 | +
|
| 116 | + // Log the event |
| 117 | + EventLogger.logEvent(msg); <4> |
| 118 | +
|
| 119 | + return confirm; |
| 120 | +} |
| 121 | +---- |
| 122 | +<1> The free-form message is a human-readable description of the event. |
| 123 | +<2> The `StructuredDataMessage` constructor takes an ID, the free-form message, and a type. |
| 124 | +<3> Developers can add key-value pairs to the message. |
| 125 | +<4> The `EventLogger` logs the event. |
| 126 | +
|
| 127 | +That way, the `transferFunds()` method logs the event with structured data |
| 128 | +by using the `EventLogger`. |
| 129 | +
|
| 130 | +== Web Application Example |
| 131 | +
|
| 132 | +In a web application, developers can use a servlet filter to populate the |
| 133 | +`ThreadContext` map with data related to the request. |
| 134 | +
|
| 135 | +The following example demonstrates how a `Filter` could investigate the request |
| 136 | +and populate the `ThreadContext` map with data such as the user's IP address, |
| 137 | +the user's login ID, the server's hostname, the product name, the locale, and the timezone. |
| 138 | +
|
| 139 | +[source, java] |
36 | 140 | ----
|
37 | 141 | import org.apache.logging.log4j.ThreadContext;
|
38 | 142 | import org.apache.commons.lang.time.DateUtils;
|
39 | 143 |
|
40 |
| -import javax.servlet.Filter; |
41 |
| -import javax.servlet.FilterConfig; |
42 |
| -import javax.servlet.ServletException; |
43 |
| -import javax.servlet.ServletRequest; |
44 |
| -import javax.servlet.ServletResponse; |
45 |
| -import javax.servlet.FilterChain; |
46 |
| -import javax.servlet.http.HttpSession; |
47 |
| -import javax.servlet.http.HttpServletRequest; |
48 |
| -import javax.servlet.http.Cookie; |
49 |
| -import javax.servlet.http.HttpServletResponse; |
| 144 | +import javax.servlet.*; |
| 145 | +import javax.servlet.http.*; |
50 | 146 | import java.io.IOException;
|
51 | 147 | import java.util.TimeZone;
|
52 | 148 |
|
53 | 149 | public class RequestFilter implements Filter {
|
54 | 150 | private FilterConfig filterConfig;
|
55 | 151 | private static String TZ_NAME = "timezoneOffset";
|
56 | 152 |
|
57 |
| - public void init(FilterConfig filterConfig) throws ServletException { |
58 |
| - this.filterConfig = filterConfig; |
59 |
| - } |
| 153 | + // Other methods ommitted for brevity |
60 | 154 |
|
61 | 155 | /**
|
62 | 156 | * Sample filter that populates the MDC on every request.
|
63 | 157 | */
|
64 | 158 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
65 | 159 | throws IOException, ServletException {
|
66 | 160 | HttpServletRequest request = (HttpServletRequest)servletRequest;
|
67 |
| - HttpServletResponse response = (HttpServletResponse)servletResponse; |
68 |
| - ThreadContext.put("ipAddress", request.getRemoteAddr()); |
| 161 | + |
| 162 | + ThreadContext.put("ipAddress", request.getRemoteAddr()); <1> |
| 163 | +
|
69 | 164 | HttpSession session = request.getSession(false);
|
70 |
| - TimeZone timeZone = null; |
71 | 165 | if (session != null) {
|
72 |
| - // Something should set this after authentication completes |
73 |
| - String loginId = (String)session.getAttribute("LoginId"); |
| 166 | + // Assuming, an authentication filter has already populated the loginId in the session |
| 167 | + String loginId = (String)session.getAttribute("loginId"); |
74 | 168 | if (loginId != null) {
|
75 | 169 | ThreadContext.put("loginId", loginId);
|
76 | 170 | }
|
77 |
| - // This assumes there is some javascript on the user's page to create the cookie. |
78 |
| - if (session.getAttribute(TZ_NAME) == null) { |
79 |
| - if (request.getCookies() != null) { |
80 |
| - for (Cookie cookie : request.getCookies()) { |
81 |
| - if (TZ_NAME.equals(cookie.getName())) { |
82 |
| - int tzOffsetMinutes = Integer.parseInt(cookie.getValue()); |
83 |
| - timeZone = TimeZone.getTimeZone("GMT"); |
84 |
| - timeZone.setRawOffset((int)(tzOffsetMinutes * DateUtils.MILLIS_PER_MINUTE)); |
85 |
| - request.getSession().setAttribute(TZ_NAME, tzOffsetMinutes); |
86 |
| - cookie.setMaxAge(0); |
87 |
| - response.addCookie(cookie); |
88 |
| - } |
89 |
| - } |
90 |
| - } |
91 |
| - } |
92 | 171 | }
|
| 172 | +
|
93 | 173 | ThreadContext.put("hostname", servletRequest.getServerName());
|
94 | 174 | ThreadContext.put("productName", filterConfig.getInitParameter("ProductName"));
|
95 | 175 | ThreadContext.put("locale", servletRequest.getLocale().getDisplayName());
|
96 |
| - if (timeZone == null) { |
97 |
| - timeZone = TimeZone.getDefault(); |
98 |
| - } |
99 |
| - ThreadContext.put("timezone", timeZone.getDisplayName()); |
| 176 | + ThreadContext.put("timezone", TimeZone.getDefault().getDisplayName()); |
| 177 | +
|
100 | 178 | filterChain.doFilter(servletRequest, servletResponse);
|
101 | 179 | ThreadContext.clear();
|
102 | 180 | }
|
103 |
| -
|
104 |
| - public void destroy() { |
105 |
| - } |
106 | 181 | }
|
107 | 182 | ----
|
| 183 | +<1> The `doFilter()` method populates the `ThreadContext` map with data related to the request. |
| 184 | +
|
| 185 | +The `Filter` needs to be registered in your `web.xml` file: |
| 186 | +
|
| 187 | +[source, xml] |
| 188 | +---- |
| 189 | +<filter> |
| 190 | + <filter-name>RequestFilter</filter-name> |
| 191 | + <filter-class>com.example.RequestFilter</filter-class> |
| 192 | + <init-param> |
| 193 | + <param-name>ProductName</param-name> |
| 194 | + <param-value>YourProductName</param-value> |
| 195 | + </init-param> |
| 196 | +</filter> |
| 197 | +<filter-mapping> |
| 198 | + <filter-name>RequestFilter</filter-name> |
| 199 | + <url-pattern>/*</url-pattern> <1> |
| 200 | +</filter-mapping> |
| 201 | +---- |
| 202 | +<1> The `RequestFilter` is mapped to all requests. |
108 | 203 |
|
109 |
| -Sample class that uses `EventLogger`. |
| 204 | +Eventually, a `Servlet` or any other related class, such as a Spring Controller, can be used to log events with structured data. |
| 205 | +The following example uses a `Servlet` to call the `EventLogger` and log a user action. |
110 | 206 |
|
111 |
| -[source,java] |
| 207 | +[source, java] |
112 | 208 | ----
|
113 |
| -import org.apache.logging.log4j.StructuredDataMessage; |
114 |
| -import org.apache.logging.log4j.EventLogger; |
115 |
| -
|
116 |
| -import java.util.Date; |
117 |
| -import java.util.UUID; |
118 |
| -
|
119 |
| -public class MyApp { |
120 |
| -
|
121 |
| - public String doFundsTransfer(Account toAccount, Account fromAccount, long amount) { |
122 |
| - toAccount.deposit(amount); |
123 |
| - fromAccount.withdraw(amount); |
124 |
| - String confirm = UUID.randomUUID().toString(); |
125 |
| - StructuredDataMessage msg = new StructuredDataMessage(confirm, null, "transfer"); |
126 |
| - msg.put("toAccount", toAccount); |
127 |
| - msg.put("fromAccount", fromAccount); |
128 |
| - msg.put("amount", amount); |
129 |
| - EventLogger.logEvent(msg); |
130 |
| - return confirm; |
| 209 | +import javax.servlet.*; |
| 210 | +import javax.servlet.http.*; |
| 211 | +import java.io.IOException; |
| 212 | +
|
| 213 | +public class UserActionServlet extends HttpServlet { |
| 214 | + |
| 215 | + protected void doPost(HttpServletRequest request, HttpServletResponse response) |
| 216 | + throws ServletException, IOException { |
| 217 | + String userId = request.getParameter("userId"); |
| 218 | + String action = request.getParameter("action"); |
| 219 | + String details = request.getParameter("details"); |
| 220 | +
|
| 221 | + // Perform and log the user action |
| 222 | + String message = "User " + userId + " performed action: " + action; |
| 223 | + StructuredDataMessage msg = new StructuredDataMessage(UUID.randomUUID().toString(), message, "userAction"); <1> |
| 224 | + msg.put("userId", userId); |
| 225 | + msg.put("action", action); |
| 226 | + msg.put("details", details); |
| 227 | +
|
| 228 | + // Log the event |
| 229 | + EventLogger.logEvent(msg); |
| 230 | +
|
| 231 | + // Respond to the client |
| 232 | + response.getWriter().write("Action logged successfully"); |
131 | 233 | }
|
132 | 234 | }
|
133 | 235 | ----
|
| 236 | +<1> `userAction` is the name of the current transaction |
| 237 | +
|
| 238 | +That way, not only the data provided to the `EventLogger` is used, but also all the |
| 239 | +data populated in the `ThreadContext` map is included in the log message. |
| 240 | +
|
| 241 | +== Benefits of Structured Logging |
| 242 | +
|
| 243 | +1. **Improved Readability and Context:** |
| 244 | + Structured logs include detailed information, making them easier to understand and analyze. |
| 245 | +2. **Better for Automated Processing:** |
| 246 | + Structured logs are easily parsed by existing log management tools. |
| 247 | +3. **Consistency:** |
| 248 | + Structured logging enforces a consistent format, helping to identify patterns in logs. |
| 249 | +4. **Performance Optimization:** |
| 250 | + Structured messages are - as all messages - only constructed when necessary, keeping overhead low. |
134 | 251 |
|
135 |
| -The `EventLogger` class uses a `Logger` named "EventLogger". `EventLogger` |
136 |
| -uses a logging level of OFF as the default to indicate that it cannot be |
137 |
| -filtered. These events can be formatted for printing using the |
138 |
| -link:../javadoc/log4j-core/org/apache/logging/log4j/core/layout/StructuredDataLayout.html[`StructuredDataLayout`]. |
|
0 commit comments