001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache license, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License. You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the license for the specific language governing permissions and
015     * limitations under the license.
016     */
017    package org.apache.logging.log4j.core.layout;
018    
019    import org.apache.logging.log4j.LoggingException;
020    import org.apache.logging.log4j.core.LogEvent;
021    import org.apache.logging.log4j.core.config.Configuration;
022    import org.apache.logging.log4j.core.config.plugins.Plugin;
023    import org.apache.logging.log4j.core.config.plugins.PluginAttr;
024    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
025    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
026    import org.apache.logging.log4j.core.net.Facility;
027    import org.apache.logging.log4j.core.net.Priority;
028    import org.apache.logging.log4j.message.Message;
029    import org.apache.logging.log4j.message.StructuredDataId;
030    import org.apache.logging.log4j.message.StructuredDataMessage;
031    import org.apache.logging.log4j.util.EnglishEnums;
032    
033    import java.net.InetAddress;
034    import java.net.NetworkInterface;
035    import java.net.SocketException;
036    import java.net.UnknownHostException;
037    import java.nio.charset.Charset;
038    import java.util.ArrayList;
039    import java.util.Calendar;
040    import java.util.Enumeration;
041    import java.util.GregorianCalendar;
042    import java.util.List;
043    import java.util.Locale;
044    import java.util.Map;
045    import java.util.SortedMap;
046    import java.util.TreeMap;
047    
048    
049    /**
050     * Formats a log event in accordance with RFC 5424.
051     */
052    @Plugin(name = "RFC5424Layout", type = "Core", elementType = "layout", printObject = true)
053    public final class RFC5424Layout extends AbstractStringLayout {
054    
055        /**
056         * Not a very good default - it is the Apache Software Foundation's enterprise number.
057         */
058        public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
059        /**
060         * The default event id.
061         */
062        public static final String DEFAULT_ID = "Audit";
063    
064        private static final String DEFAULT_MDCID = "mdc";
065        private static final int TWO_DIGITS = 10;
066        private static final int THREE_DIGITS = 100;
067        private static final int MILLIS_PER_MINUTE = 60000;
068        private static final int MINUTES_PER_HOUR = 60;
069    
070        private final Facility facility;
071        private final String defaultId;
072        private final Integer enterpriseNumber;
073        private final boolean includeMDC;
074        private final String mdcId;
075        private final String localHostName;
076        private final String appName;
077        private final String messageId;
078        private final String configName;
079        private final List<String> mdcExcludes;
080        private final List<String> mdcIncludes;
081        private final List<String> mdcRequired;
082        private final ListChecker checker;
083        private final ListChecker noopChecker = new NoopChecker();
084        private final boolean includeNewLine;
085    
086        private long lastTimestamp = -1;
087        private String timestamppStr;
088    
089    
090        private RFC5424Layout(Configuration config, Facility facility, String id, int ein, boolean includeMDC,
091                              boolean includeNL, String mdcId, String appName, String messageId, String excludes,
092                              String includes, String required, Charset charset) {
093            super(charset);
094            this.facility = facility;
095            this.defaultId = id == null ? DEFAULT_ID : id;
096            this.enterpriseNumber = ein;
097            this.includeMDC = includeMDC;
098            this.includeNewLine = includeNL;
099            this.mdcId = mdcId;
100            this.appName = appName;
101            this.messageId = messageId;
102            this.localHostName = getLocalHostname();
103            ListChecker c = null;
104            if (excludes != null) {
105                String[] array = excludes.split(",");
106                if (array.length > 0) {
107                    c = new ExcludeChecker();
108                    mdcExcludes = new ArrayList<String>(array.length);
109                    for (String str : array) {
110                        mdcExcludes.add(str.trim());
111                    }
112                } else {
113                    mdcExcludes = null;
114                }
115            } else {
116                mdcExcludes = null;
117            }
118            if (includes != null) {
119                String[] array = includes.split(",");
120                if (array.length > 0) {
121                    c = new IncludeChecker();
122                    mdcIncludes = new ArrayList<String>(array.length);
123                    for (String str : array) {
124                        mdcIncludes.add(str.trim());
125                    }
126                } else {
127                    mdcIncludes = null;
128                }
129            } else {
130                mdcIncludes = null;
131            }
132            if (required != null) {
133                String[] array = required.split(",");
134                if (array.length > 0) {
135                    mdcRequired = new ArrayList<String>(array.length);
136                    for (String str : array) {
137                        mdcRequired.add(str.trim());
138                    }
139                } else {
140                    mdcRequired = null;
141                }
142    
143            } else {
144                mdcRequired = null;
145            }
146            this.checker = c != null ? c : noopChecker;
147            String name = config == null ? null : config.getName();
148            configName = (name != null && name.length() > 0) ? name : null;
149        }
150    
151        /**
152         * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification.
153         *
154         * @param event The LogEvent.
155         * @return The RFC 5424 String representation of the LogEvent.
156         */
157        public String toSerializable(final LogEvent event) {
158            Message msg = event.getMessage();
159            boolean isStructured = msg instanceof StructuredDataMessage;
160            StringBuilder buf = new StringBuilder();
161    
162            buf.append("<");
163            buf.append(Priority.getPriority(facility, event.getLevel()));
164            buf.append(">1 ");
165            buf.append(computeTimeStampString(event.getMillis()));
166            buf.append(' ');
167            buf.append(localHostName);
168            buf.append(' ');
169            if (appName != null) {
170                buf.append(appName);
171            } else if (configName != null) {
172                buf.append(configName);
173            } else {
174                buf.append("-");
175            }
176            buf.append(" ");
177            buf.append(getProcId());
178            buf.append(" ");
179            String type = isStructured ? ((StructuredDataMessage) msg).getType() : null;
180            if (type != null) {
181                buf.append(type);
182            } else if (messageId != null) {
183                buf.append(messageId);
184            } else {
185                buf.append("-");
186            }
187            buf.append(" ");
188            if (isStructured || includeMDC) {
189                StructuredDataId id = null;
190                String text;
191                if (isStructured) {
192                    StructuredDataMessage data = (StructuredDataMessage) msg;
193                    Map<String, String> map = data.getData();
194                    id = data.getId();
195                    formatStructuredElement(id, map, buf, noopChecker);
196                    text = data.getFormat();
197                } else {
198                    text = msg.getFormattedMessage();
199                }
200                if (includeMDC) {
201                    if (mdcRequired != null) {
202                        checkRequired(event.getContextMap());
203                    }
204                    int ein = id == null || id.getEnterpriseNumber() < 0 ? enterpriseNumber : id.getEnterpriseNumber();
205                    StructuredDataId mdcSDID = new StructuredDataId(mdcId, ein, null, null);
206                    formatStructuredElement(mdcSDID, event.getContextMap(), buf, checker);
207                }
208                if (text != null && text.length() > 0) {
209                    buf.append(" ").append(text);
210                }
211            } else {
212                buf.append("- ");
213                buf.append(msg.getFormattedMessage());
214            }
215            if (includeNewLine) {
216                buf.append("\n");
217            }
218            return buf.toString();
219        }
220    
221        protected String getProcId() {
222            return "-";
223        }
224    
225        /**
226         * This method gets the network name of the machine we are running on.
227         * Returns "UNKNOWN_LOCALHOST" in the unlikely case where the host name
228         * cannot be found.
229         *
230         * @return String the name of the local host
231         */
232        public String getLocalHostname() {
233            try {
234                InetAddress addr = InetAddress.getLocalHost();
235                return addr.getHostName();
236            } catch (UnknownHostException uhe) {
237                try {
238                    Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
239                    while (interfaces.hasMoreElements()) {
240                        NetworkInterface nic = interfaces.nextElement();
241                        Enumeration<InetAddress> addresses = nic.getInetAddresses();
242                        while (addresses.hasMoreElements()) {
243                            InetAddress address = addresses.nextElement();
244                            if (!address.isLoopbackAddress()) {
245                                String hostname = address.getHostName();
246                                if (hostname != null) {
247                                    return hostname;
248                                }
249                            }
250                        }
251                    }
252                } catch (SocketException se) {
253                    LOGGER.error("Could not determine local host name", uhe);
254                    return "UNKNOWN_LOCALHOST";
255                }
256                LOGGER.error("Could not determine local host name", uhe);
257                return "UNKNOWN_LOCALHOST";
258            }
259        }
260    
261        protected List<String> getMdcExcludes() {
262            return mdcExcludes;
263        }
264    
265        protected List<String> getMdcIncludes() {
266            return mdcIncludes;
267        }
268    
269        private String computeTimeStampString(long now) {
270            long last;
271            synchronized (this) {
272                last = lastTimestamp;
273                if (now == lastTimestamp) {
274                    return timestamppStr;
275                }
276            }
277    
278            StringBuilder buf = new StringBuilder();
279            Calendar cal = new GregorianCalendar();
280            cal.setTimeInMillis(now);
281            buf.append(Integer.toString(cal.get(Calendar.YEAR)));
282            buf.append("-");
283            pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buf);
284            buf.append("-");
285            pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buf);
286            buf.append("T");
287            pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buf);
288            buf.append(":");
289            pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buf);
290            buf.append(":");
291            pad(cal.get(Calendar.SECOND), TWO_DIGITS, buf);
292    
293            int millis = cal.get(Calendar.MILLISECOND);
294            if (millis != 0) {
295                buf.append('.');
296                pad(millis, THREE_DIGITS, buf);
297            }
298    
299            int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE;
300            if (tzmin == 0) {
301                buf.append("Z");
302            } else {
303                if (tzmin < 0) {
304                    tzmin = -tzmin;
305                    buf.append("-");
306                } else {
307                    buf.append("+");
308                }
309                int tzhour = tzmin / MINUTES_PER_HOUR;
310                tzmin -= tzhour * MINUTES_PER_HOUR;
311                pad(tzhour, TWO_DIGITS, buf);
312                buf.append(":");
313                pad(tzmin, TWO_DIGITS, buf);
314            }
315            synchronized (this) {
316                if (last == lastTimestamp) {
317                    lastTimestamp = now;
318                    timestamppStr = buf.toString();
319                }
320            }
321            return buf.toString();
322        }
323    
324        private void pad(int val, int max, StringBuilder buf) {
325            while (max > 1) {
326                if (val < max) {
327                    buf.append("0");
328                }
329                max = max / TWO_DIGITS;
330            }
331            buf.append(Integer.toString(val));
332        }
333    
334        private void formatStructuredElement(StructuredDataId id, Map<String, String> data, StringBuilder sb,
335                                             ListChecker checker) {
336            if (id == null && defaultId == null) {
337                return;
338            }
339            sb.append("[");
340            sb.append(getId(id));
341            appendMap(data, sb, checker);
342            sb.append("]");
343        }
344    
345        private String getId(StructuredDataId id) {
346            StringBuilder sb = new StringBuilder();
347            if (id.getName() == null) {
348                sb.append(defaultId);
349            } else {
350                sb.append(id.getName());
351            }
352            int ein = id.getEnterpriseNumber();
353            if (ein < 0) {
354                ein = enterpriseNumber;
355            }
356            if (ein >= 0) {
357                sb.append("@").append(ein);
358            }
359            return sb.toString();
360        }
361    
362        private void checkRequired(Map<String, String> map) {
363            for (String key : mdcRequired) {
364                String value = map.get(key);
365                if (value == null) {
366                    throw new LoggingException("Required key " + key + " is missing from the " + mdcId);
367                }
368            }
369        }
370    
371        private void appendMap(Map<String, String> map, StringBuilder sb, ListChecker checker)
372        {
373            SortedMap<String, String> sorted = new TreeMap<String, String>(map);
374            for (Map.Entry<String, String> entry : sorted.entrySet()) {
375                if (checker.check(entry.getKey())) {
376                    sb.append(" ");
377                    sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
378                }
379            }
380        }
381    
382        /**
383         * Interface used to check keys in a Map.
384         */
385        private interface ListChecker {
386            boolean check(String key);
387        }
388    
389        /**
390         * Includes only the listed keys.
391         */
392        private class IncludeChecker implements ListChecker {
393            public boolean check(String key) {
394                return mdcIncludes.contains(key);
395            }
396        }
397    
398        /**
399         * Excludes the listed keys.
400         */
401        private class ExcludeChecker implements ListChecker {
402            public boolean check(String key) {
403                return !mdcExcludes.contains(key);
404            }
405        }
406    
407        /**
408         * Does nothing.
409         */
410        private class NoopChecker implements ListChecker {
411            public boolean check(String key) {
412                return true;
413            }
414        }
415    
416        @Override
417        public String toString() {
418            StringBuilder sb = new StringBuilder();
419            sb.append("facility=").append(facility.name());
420            sb.append(" appName=").append(appName);
421            sb.append(" defaultId=").append(defaultId);
422            sb.append(" enterpriseNumber=").append(enterpriseNumber);
423            sb.append(" newLine=").append(includeNewLine);
424            sb.append(" includeMDC=").append(includeMDC);
425            sb.append(" messageId=").append(messageId);
426            return sb.toString();
427        }
428    
429        /**
430         * Create the RFC 5424 Layout.
431         * @param facility The Facility is used to try to classify the message.
432         * @param id The default structured data id to use when formatting according to RFC 5424.
433         * @param ein The IANA enterprise number.
434         * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog
435         * record. Defaults to "true:.
436         * @param mdcId The id to use for the MDC Structured Data Element.
437         * @param includeNL If true, a newline will be appended to the end of the syslog record. The default is false.
438         * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record.
439         * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records.
440         * @param excludes A comma separated list of mdc keys that should be excluded from the LogEvent.
441         * @param includes A comma separated list of mdc keys that should be included in the FlumeEvent.
442         * @param required A comma separated list of mdc keys that must be present in the MDC.
443         * @param charset The character set.
444         * @param config The Configuration. Some Converters require access to the Interpolator.
445         * @return An RFC5424Layout.
446         */
447        @PluginFactory
448        public static RFC5424Layout createLayout(@PluginAttr("facility") String facility,
449                                                 @PluginAttr("id") String id,
450                                                 @PluginAttr("enterpriseNumber") String ein,
451                                                 @PluginAttr("includeMDC") String includeMDC,
452                                                 @PluginAttr("mdcId") String mdcId,
453                                                 @PluginAttr("newLine") String includeNL,
454                                                 @PluginAttr("appName") String appName,
455                                                 @PluginAttr("messageId") String msgId,
456                                                 @PluginAttr("mdcExcludes") String excludes,
457                                                 @PluginAttr("mdcIncludes") String includes,
458                                                 @PluginAttr("mdcRequired") String required,
459                                                 @PluginAttr("charset") String charset,
460                                                 @PluginConfiguration Configuration config) {
461            Charset c = Charset.isSupported("UTF-8") ? Charset.forName("UTF-8") : Charset.defaultCharset();
462            if (charset != null) {
463                if (Charset.isSupported(charset)) {
464                    c = Charset.forName(charset);
465                } else {
466                    LOGGER.error("Charset " + charset + " is not supported for layout, using " + c.displayName());
467                }
468            }
469            if (includes != null && excludes != null) {
470                LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored");
471                includes = null;
472            }
473            Facility f = Facility.toFacility(facility, Facility.LOCAL0);
474            int enterpriseNumber = ein == null ? DEFAULT_ENTERPRISE_NUMBER : Integer.parseInt(ein);
475            boolean isMdc = includeMDC == null ? true : Boolean.valueOf(includeMDC);
476            boolean includeNewLine = includeNL == null ? false : Boolean.valueOf(includeNL);
477            if (mdcId == null) {
478                mdcId = DEFAULT_MDCID;
479            }
480    
481            return new RFC5424Layout(config, f, id, enterpriseNumber, isMdc, includeNewLine, mdcId, appName, msgId,
482                                     excludes, includes, required, c);
483        }
484    }