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
018package org.apache.log4j.net;
019
020import org.apache.log4j.AppenderSkeleton;
021import org.apache.log4j.Layout;
022import org.apache.log4j.helpers.SyslogQuietWriter;
023import org.apache.log4j.helpers.SyslogWriter;
024import org.apache.log4j.spi.LoggingEvent;
025
026import java.text.SimpleDateFormat;
027import java.util.Date;
028import java.util.Locale;
029import java.net.InetAddress;
030import java.net.UnknownHostException;
031import java.io.IOException;
032
033// Contributors: Yves Bossel <ybossel@opengets.cl>
034//               Christopher Taylor <cstaylor@pacbell.net>
035
036/**
037    Use SyslogAppender to send log messages to a remote syslog daemon.
038
039    @author Ceki G&uuml;lc&uuml;
040    @author Anders Kristensen
041 */
042public class SyslogAppender extends AppenderSkeleton {
043  // The following constants are extracted from a syslog.h file
044  // copyrighted by the Regents of the University of California
045  // I hope nobody at Berkley gets offended.
046
047  /** Kernel messages */
048  final static public int LOG_KERN     = 0;
049  /** Random user-level messages */
050  final static public int LOG_USER     = 1<<3;
051  /** Mail system */
052  final static public int LOG_MAIL     = 2<<3;
053  /** System daemons */
054  final static public int LOG_DAEMON   = 3<<3;
055  /** security/authorization messages */
056  final static public int LOG_AUTH     = 4<<3;
057  /** messages generated internally by syslogd */
058  final static public int LOG_SYSLOG   = 5<<3;
059
060  /** line printer subsystem */
061  final static public int LOG_LPR      = 6<<3;
062  /** network news subsystem */
063  final static public int LOG_NEWS     = 7<<3;
064  /** UUCP subsystem */
065  final static public int LOG_UUCP     = 8<<3;
066  /** clock daemon */
067  final static public int LOG_CRON     = 9<<3;
068  /** security/authorization  messages (private) */
069  final static public int LOG_AUTHPRIV = 10<<3;
070  /** ftp daemon */
071  final static public int LOG_FTP      = 11<<3;
072
073  // other codes through 15 reserved for system use
074  /** reserved for local use */
075  final static public int LOG_LOCAL0 = 16<<3;
076  /** reserved for local use */
077  final static public int LOG_LOCAL1 = 17<<3;
078  /** reserved for local use */
079  final static public int LOG_LOCAL2 = 18<<3;
080  /** reserved for local use */
081  final static public int LOG_LOCAL3 = 19<<3;
082  /** reserved for local use */
083  final static public int LOG_LOCAL4 = 20<<3;
084  /** reserved for local use */
085  final static public int LOG_LOCAL5 = 21<<3;
086  /** reserved for local use */
087  final static public int LOG_LOCAL6 = 22<<3;
088  /** reserved for local use*/
089  final static public int LOG_LOCAL7 = 23<<3;
090
091  protected static final int SYSLOG_HOST_OI = 0;
092  protected static final int FACILITY_OI = 1;
093
094  static final String TAB = "    ";
095
096  // Have LOG_USER as default
097  int syslogFacility = LOG_USER;
098  String facilityStr;
099  boolean facilityPrinting = false;
100
101  //SyslogTracerPrintWriter stp;
102  SyslogQuietWriter sqw;
103  String syslogHost;
104
105    /**
106     * If true, the appender will generate the HEADER (timestamp and host name)
107     * part of the syslog packet.
108     * @since 1.2.15
109     */
110  private boolean header = false;
111    /**
112     * Date format used if header = true.
113     * @since 1.2.15
114     */
115  private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd HH:mm:ss ", Locale.ENGLISH);
116    /**
117     * Host name used to identify messages from this appender.
118     * @since 1.2.15
119     */
120  private String localHostname;
121
122    /**
123     * Set to true after the header of the layout has been sent or if it has none.
124     */
125  private boolean layoutHeaderChecked = false;
126
127  public
128  SyslogAppender() {
129    this.initSyslogFacilityStr();
130  }
131
132  public
133  SyslogAppender(Layout layout, int syslogFacility) {
134    this.layout = layout;
135    this.syslogFacility = syslogFacility;
136    this.initSyslogFacilityStr();
137  }
138
139  public
140  SyslogAppender(Layout layout, String syslogHost, int syslogFacility) {
141    this(layout, syslogFacility);
142    setSyslogHost(syslogHost);
143  }
144
145  /**
146     Release any resources held by this SyslogAppender.
147
148     @since 0.8.4
149   */
150  synchronized
151  public
152  void close() {
153    closed = true;
154    if (sqw != null) {
155        try {
156            if (layoutHeaderChecked && layout != null && layout.getFooter() != null) {
157                sendLayoutMessage(layout.getFooter());
158            }
159            sqw.close();
160            sqw = null;
161        } catch(java.io.InterruptedIOException e) {
162            Thread.currentThread().interrupt();
163            sqw = null;
164        } catch(IOException e) {
165            sqw = null;
166        }
167    }
168  }
169
170  private
171  void initSyslogFacilityStr() {
172    facilityStr = getFacilityString(this.syslogFacility);
173
174    if (facilityStr == null) {
175      System.err.println("\"" + syslogFacility +
176                  "\" is an unknown syslog facility. Defaulting to \"USER\".");
177      this.syslogFacility = LOG_USER;
178      facilityStr = "user:";
179    } else {
180      facilityStr += ":";
181    }
182  }
183
184  /**
185     Returns the specified syslog facility as a lower-case String,
186     e.g. "kern", "user", etc.
187  */
188  public
189  static
190  String getFacilityString(int syslogFacility) {
191    switch(syslogFacility) {
192    case LOG_KERN:      return "kern";
193    case LOG_USER:      return "user";
194    case LOG_MAIL:      return "mail";
195    case LOG_DAEMON:    return "daemon";
196    case LOG_AUTH:      return "auth";
197    case LOG_SYSLOG:    return "syslog";
198    case LOG_LPR:       return "lpr";
199    case LOG_NEWS:      return "news";
200    case LOG_UUCP:      return "uucp";
201    case LOG_CRON:      return "cron";
202    case LOG_AUTHPRIV:  return "authpriv";
203    case LOG_FTP:       return "ftp";
204    case LOG_LOCAL0:    return "local0";
205    case LOG_LOCAL1:    return "local1";
206    case LOG_LOCAL2:    return "local2";
207    case LOG_LOCAL3:    return "local3";
208    case LOG_LOCAL4:    return "local4";
209    case LOG_LOCAL5:    return "local5";
210    case LOG_LOCAL6:    return "local6";
211    case LOG_LOCAL7:    return "local7";
212    default:            return null;
213    }
214  }
215
216  /**
217     Returns the integer value corresponding to the named syslog
218     facility, or -1 if it couldn't be recognized.
219
220     @param facilityName one of the strings KERN, USER, MAIL, DAEMON,
221            AUTH, SYSLOG, LPR, NEWS, UUCP, CRON, AUTHPRIV, FTP, LOCAL0,
222            LOCAL1, LOCAL2, LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7.
223            The matching is case-insensitive.
224
225     @since 1.1
226  */
227  public
228  static
229  int getFacility(String facilityName) {
230    if(facilityName != null) {
231      facilityName = facilityName.trim();
232    }
233    if("KERN".equalsIgnoreCase(facilityName)) {
234      return LOG_KERN;
235    } else if("USER".equalsIgnoreCase(facilityName)) {
236      return LOG_USER;
237    } else if("MAIL".equalsIgnoreCase(facilityName)) {
238      return LOG_MAIL;
239    } else if("DAEMON".equalsIgnoreCase(facilityName)) {
240      return LOG_DAEMON;
241    } else if("AUTH".equalsIgnoreCase(facilityName)) {
242      return LOG_AUTH;
243    } else if("SYSLOG".equalsIgnoreCase(facilityName)) {
244      return LOG_SYSLOG;
245    } else if("LPR".equalsIgnoreCase(facilityName)) {
246      return LOG_LPR;
247    } else if("NEWS".equalsIgnoreCase(facilityName)) {
248      return LOG_NEWS;
249    } else if("UUCP".equalsIgnoreCase(facilityName)) {
250      return LOG_UUCP;
251    } else if("CRON".equalsIgnoreCase(facilityName)) {
252      return LOG_CRON;
253    } else if("AUTHPRIV".equalsIgnoreCase(facilityName)) {
254      return LOG_AUTHPRIV;
255    } else if("FTP".equalsIgnoreCase(facilityName)) {
256      return LOG_FTP;
257    } else if("LOCAL0".equalsIgnoreCase(facilityName)) {
258      return LOG_LOCAL0;
259    } else if("LOCAL1".equalsIgnoreCase(facilityName)) {
260      return LOG_LOCAL1;
261    } else if("LOCAL2".equalsIgnoreCase(facilityName)) {
262      return LOG_LOCAL2;
263    } else if("LOCAL3".equalsIgnoreCase(facilityName)) {
264      return LOG_LOCAL3;
265    } else if("LOCAL4".equalsIgnoreCase(facilityName)) {
266      return LOG_LOCAL4;
267    } else if("LOCAL5".equalsIgnoreCase(facilityName)) {
268      return LOG_LOCAL5;
269    } else if("LOCAL6".equalsIgnoreCase(facilityName)) {
270      return LOG_LOCAL6;
271    } else if("LOCAL7".equalsIgnoreCase(facilityName)) {
272      return LOG_LOCAL7;
273    } else {
274      return -1;
275    }
276  }
277
278
279  private void splitPacket(final String header, final String packet) {
280      int byteCount = packet.getBytes().length;
281      //
282      //   if packet is less than RFC 3164 limit
283      //      of 1024 bytes, then write it
284      //      (must allow for up 5to 5 characters in the PRI section
285      //          added by SyslogQuietWriter)
286      if (byteCount <= 1019) {
287          sqw.write(packet);
288      } else {
289          int split = header.length() + (packet.length() - header.length())/2;
290          splitPacket(header, packet.substring(0, split) + "...");
291          splitPacket(header, header + "..." + packet.substring(split));
292      }      
293  }
294
295  public
296  void append(LoggingEvent event) {
297
298    if(!isAsSevereAsThreshold(event.getLevel()))
299      return;
300
301    // We must not attempt to append if sqw is null.
302    if(sqw == null) {
303      errorHandler.error("No syslog host is set for SyslogAppedender named \""+
304                        this.name+"\".");
305      return;
306    }
307
308    if (!layoutHeaderChecked) {
309        if (layout != null && layout.getHeader() != null) {
310            sendLayoutMessage(layout.getHeader());
311        }
312        layoutHeaderChecked = true;
313    }
314
315    String hdr = getPacketHeader(event.timeStamp);
316    String packet;
317    if (layout == null) {
318        packet = String.valueOf(event.getMessage());
319    } else {
320        packet = layout.format(event);
321    }
322    if(facilityPrinting || hdr.length() > 0) {
323        StringBuffer buf = new StringBuffer(hdr);
324        if(facilityPrinting) {
325            buf.append(facilityStr);
326        }
327        buf.append(packet);
328        packet = buf.toString();
329    }
330
331    sqw.setLevel(event.getLevel().getSyslogEquivalent());
332    //
333    //   if message has a remote likelihood of exceeding 1024 bytes
334    //      when encoded, consider splitting message into multiple packets
335    if (packet.length() > 256) {
336        splitPacket(hdr, packet);
337    } else {
338        sqw.write(packet);
339    }
340
341    if (layout == null || layout.ignoresThrowable()) {
342      String[] s = event.getThrowableStrRep();
343      if (s != null) {
344        for(int i = 0; i < s.length; i++) {
345            if (s[i].startsWith("\t")) {
346               sqw.write(hdr+TAB+s[i].substring(1));
347            } else {
348               sqw.write(hdr+s[i]);
349            }
350        }
351      }
352    }
353  }
354
355  /**
356     This method returns immediately as options are activated when they
357     are set.
358  */
359  public
360  void activateOptions() {
361      if (header) {
362        getLocalHostname();
363      }
364      if (layout != null && layout.getHeader() != null) {
365          sendLayoutMessage(layout.getHeader());
366      }
367      layoutHeaderChecked = true;
368  }
369
370  /**
371     The SyslogAppender requires a layout. Hence, this method returns
372     <code>true</code>.
373
374     @since 0.8.4 */
375  public
376  boolean requiresLayout() {
377    return true;
378  }
379
380  /**
381    The <b>SyslogHost</b> option is the name of the the syslog host
382    where log output should go.  A non-default port can be specified by
383    appending a colon and port number to a host name,
384    an IPv4 address or an IPv6 address enclosed in square brackets.
385
386    <b>WARNING</b> If the SyslogHost is not set, then this appender
387    will fail.
388   */
389  public
390  void setSyslogHost(final String syslogHost) {
391    this.sqw = new SyslogQuietWriter(new SyslogWriter(syslogHost),
392                                     syslogFacility, errorHandler);
393    //this.stp = new SyslogTracerPrintWriter(sqw);
394    this.syslogHost = syslogHost;
395  }
396
397  /**
398     Returns the value of the <b>SyslogHost</b> option.
399   */
400  public
401  String getSyslogHost() {
402    return syslogHost;
403  }
404
405  /**
406     Set the syslog facility. This is the <b>Facility</b> option.
407
408     <p>The <code>facilityName</code> parameter must be one of the
409     strings KERN, USER, MAIL, DAEMON, AUTH, SYSLOG, LPR, NEWS, UUCP,
410     CRON, AUTHPRIV, FTP, LOCAL0, LOCAL1, LOCAL2, LOCAL3, LOCAL4,
411     LOCAL5, LOCAL6, LOCAL7. Case is unimportant.
412
413     @since 0.8.1 */
414  public
415  void setFacility(String facilityName) {
416    if(facilityName == null)
417      return;
418
419    syslogFacility = getFacility(facilityName);
420    if (syslogFacility == -1) {
421      System.err.println("["+facilityName +
422                  "] is an unknown syslog facility. Defaulting to [USER].");
423      syslogFacility = LOG_USER;
424    }
425
426    this.initSyslogFacilityStr();
427
428    // If there is already a sqw, make it use the new facility.
429    if(sqw != null) {
430      sqw.setSyslogFacility(this.syslogFacility);
431    }
432  }
433
434  /**
435     Returns the value of the <b>Facility</b> option.
436   */
437  public
438  String getFacility() {
439    return getFacilityString(syslogFacility);
440  }
441
442  /**
443    If the <b>FacilityPrinting</b> option is set to true, the printed
444    message will include the facility name of the application. It is
445    <em>false</em> by default.
446   */
447  public
448  void setFacilityPrinting(boolean on) {
449    facilityPrinting = on;
450  }
451
452  /**
453     Returns the value of the <b>FacilityPrinting</b> option.
454   */
455  public
456  boolean getFacilityPrinting() {
457    return facilityPrinting;
458  }
459
460  /**
461   * If true, the appender will generate the HEADER part (that is, timestamp and host name)
462   * of the syslog packet.  Default value is false for compatibility with existing behavior,
463   * however should be true unless there is a specific justification.
464   * @since 1.2.15
465  */
466  public final boolean getHeader() {
467      return header;
468  }
469
470    /**
471     * Returns whether the appender produces the HEADER part (that is, timestamp and host name)
472     * of the syslog packet.
473     * @since 1.2.15
474    */
475  public final void setHeader(final boolean val) {
476      header = val;
477  }
478
479    /**
480     * Get the host name used to identify this appender.
481     * @return local host name
482     * @since 1.2.15
483     */
484  private String getLocalHostname() {
485      if (localHostname == null) {
486          try {
487            InetAddress addr = InetAddress.getLocalHost();
488            localHostname = addr.getHostName();
489          } catch (UnknownHostException uhe) {
490            localHostname = "UNKNOWN_HOST";
491          }
492      }
493      return localHostname;
494  }
495
496    /**
497     * Gets HEADER portion of packet.
498     * @param timeStamp number of milliseconds after the standard base time.
499     * @return HEADER portion of packet, will be zero-length string if header is false.
500     * @since 1.2.15
501     */
502  private String getPacketHeader(final long timeStamp) {
503      if (header) {
504        StringBuffer buf = new StringBuffer(dateFormat.format(new Date(timeStamp)));
505        //  RFC 3164 says leading space, not leading zero on days 1-9
506        if (buf.charAt(4) == '0') {
507          buf.setCharAt(4, ' ');
508        }
509        buf.append(getLocalHostname());
510        buf.append(' ');
511        return buf.toString();
512      }
513      return "";
514  }
515
516    /**
517     * Set header or footer of layout.
518     * @param msg message body, may not be null.
519     */
520  private void sendLayoutMessage(final String msg) {
521      if (sqw != null) {
522          String packet = msg;
523          String hdr = getPacketHeader(new Date().getTime());
524          if(facilityPrinting || hdr.length() > 0) {
525              StringBuffer buf = new StringBuffer(hdr);
526              if(facilityPrinting) {
527                  buf.append(facilityStr);
528              }
529              buf.append(msg);
530              packet = buf.toString();
531          }
532          sqw.setLevel(6);
533          sqw.write(packet);
534      }
535  }
536}