/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.plugins.platform; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.domain.configuration.PropertyMap; import org.rhq.core.domain.event.Event; import org.rhq.core.domain.event.EventSeverity; import org.rhq.core.pluginapi.event.EventContext; import org.rhq.core.pluginapi.inventory.ResourceContext; /** * Processes syslog messages and prepares them as events. * Subclasses need to extend this in order to get the actual syslog messages from some source. * * @author Greg Hinkle * @author John Mazzitelli */ public class SyslogProcessor { private final Log log = LogFactory.getLog(SyslogProcessor.class); private final EventContext eventContext; private final EventSeverity minimumEventSeverity; private final Pattern includesPattern; private final Pattern parserRegex; private final SimpleDateFormat dateTimeFormatter; private final String sourceLocation; protected static final String EVENT_LOG_TYPE = "Event Log"; // as defined in plugin descriptor public SyslogProcessor(ResourceContext resourceContext, PropertyMap logProperties, String sourceLocation) { this.sourceLocation = sourceLocation; this.eventContext = resourceContext.getEventContext(); String minSev = logProperties.getSimpleValue(LinuxPlatformComponent.PLUGIN_CONFIG_EVENT_TRACKING_MIN_SEV, ""); if (minSev.toLowerCase().startsWith("info")) { minimumEventSeverity = EventSeverity.INFO; } else if (minSev.toLowerCase().startsWith("warn")) { minimumEventSeverity = EventSeverity.WARN; } else if (minSev.toLowerCase().startsWith("err")) { minimumEventSeverity = EventSeverity.ERROR; } else { minimumEventSeverity = null; } String regexString = logProperties.getSimpleValue( LinuxPlatformComponent.PLUGIN_CONFIG_EVENT_TRACKING_INCLUDES_REGEX, null); Pattern regexPattern = null; if (regexString != null && !regexString.equals("")) { try { regexPattern = Pattern.compile(regexString); } catch (Exception e) { log.error("Invalid includes regex [" + regexString + "]. All events will be accepted. " + e); } } this.includesPattern = regexPattern; regexString = logProperties.getSimpleValue(LinuxPlatformComponent.PLUGIN_CONFIG_EVENT_TRACKING_PARSER_REGEX, null); regexPattern = null; if (regexString != null && !regexString.equals("")) { try { regexPattern = Pattern.compile(regexString); } catch (Exception e) { log.error("Invalid parser regex [" + regexString + "]. Will parse with a best guess. " + e); } } this.parserRegex = regexPattern; regexString = logProperties.getSimpleValue(LinuxPlatformComponent.PLUGIN_CONFIG_EVENT_TRACKING_DATETIME_FORMAT, null); SimpleDateFormat formatter = null; if (regexString != null && !regexString.equals("")) { try { formatter = new SimpleDateFormat(regexString); } catch (Exception e) { log.error("Invalid datetime format [" + regexString + "]. Will use current times. " + e); } } this.dateTimeFormatter = formatter; return; } protected String getSourceLocation() { return this.sourceLocation; } protected EventContext getEventContext() { return this.eventContext; } protected EventSeverity getMinimumEventSeverity() { return this.minimumEventSeverity; } protected Pattern getIncludesPattern() { return this.includesPattern; } protected Pattern getParserRegex() { return this.parserRegex; } /** * Converts the givem syslog message to an event. * If the parser regular expression was specified, it will be used to parse the message. If it was not * specified, this method will attempt to handle this format: * * "%timegenerated:::date-rfc3339%,%syslogpriority-text%,%syslogfacility-text%:%msg%\n" * * If that fails, the entire line will be used as the message detail, with the timestamp being the current * time and the severity being INFO. * * @param syslogMessage the actual syslog message * * @return the event, or <code>null</code> if the event didn't match our regex, wasn't of the minimum severity * or couldn't be parsed successfully */ protected Event convertLine(String syslogMessage) { Event event = null; try { if (this.parserRegex != null) { event = convertLineParserRegEx(syslogMessage); } else { event = convertLineDefaultFormat(syslogMessage); // no parser regex, use best attempt with our default format } // if we could not parse the event, just use the entire log as the event detail if (event == null) { event = convertAnyLine(syslogMessage); } // filter the event if it doesn't meet our minimum severity requirement or it doesn't match the includes regex if (event != null) { if (this.minimumEventSeverity != null && !event.getSeverity().isAtLeastAsSevereAs(this.minimumEventSeverity)) { if (log.isTraceEnabled()) { log.trace("event is not at minimum severity: " + event); } event = null; } else if (this.includesPattern != null && !this.includesPattern.matcher(event.getDetail()).matches()) { if (log.isTraceEnabled()) { log.trace("event does not match includes pattern [" + this.includesPattern + "]: " + event); } event = null; } } } catch (Exception e) { event = null; log.warn("Failed to convert syslog message [" + syslogMessage + "] to event: " + e); } return event; } protected Event convertLineParserRegEx(String syslogMessage) { Event event; try { Matcher matcher = this.parserRegex.matcher(syslogMessage); if (matcher.find() && (matcher.groupCount() == 3)) { String dateTimeString = matcher.group(1); String severityString = matcher.group(2); String detailsString = matcher.group(3); long timestamp = getTimestamp(dateTimeString); EventSeverity severity = getSeverity(severityString); event = new Event(EVENT_LOG_TYPE, this.sourceLocation, timestamp, severity, detailsString); } else { event = null; if (log.isTraceEnabled()) { log.trace("Message [" + syslogMessage + "] did not match parser regex: " + this.parserRegex.pattern()); } } } catch (Exception e) { event = null; if (log.isTraceEnabled()) { log.trace("Failed to parse [" + syslogMessage + "] with [" + this.parserRegex.pattern() + "]", e); } } return event; } protected Event convertLineDefaultFormat(String syslogMessage) { // "%timegenerated:::date-rfc3339%,%syslogpriority-text%,%syslogfacility-text%:%msg%\n" Event event; try { String[] messageParts = syslogMessage.split("\\,", 3); if (messageParts == null || messageParts.length < 3) { return null; // message doesn't seem to match the syntax of our default format } String dateTimeString = messageParts[0]; String severityString = messageParts[1]; String detailsString = messageParts[2]; long timestamp = getTimestamp(dateTimeString); EventSeverity severity = getSeverity(severityString); event = new Event(EVENT_LOG_TYPE, this.sourceLocation, timestamp, severity, detailsString); } catch (Exception e) { event = null; if (log.isTraceEnabled()) { log.trace("defaultFormat: Failed to convert syslog message [" + syslogMessage + "] to event: " + e); } } return event; } protected Event convertAnyLine(String syslogMessage) { Event event; try { long timestamp = System.currentTimeMillis(); EventSeverity severity = EventSeverity.INFO; event = new Event(EVENT_LOG_TYPE, this.sourceLocation, timestamp, severity, syslogMessage); } catch (Exception e) { event = null; if (log.isTraceEnabled()) { log.trace("anyLine: Failed to convert syslog message [" + syslogMessage + "] to event: " + e); } } return event; } /** * Given a date/time stamp, this will parse it using the configured date/time format. * If no format is specified or an error occurs, the current time will be returned. * However, if no format is specified but the date string is specified in RFC3339 format, * this method will parse it as such. * RFC3339 format is like "YYYY-MM-DDTHH:mm:ss-00:00" where 'T' separates the date and time. * * @param dateTimeString the date-time string to parse and return its epoch millis representation * @return the epoch millis that corresponds to the given time or current time otherwise */ protected long getTimestamp(String dateTimeString) { if (dateTimeString == null || dateTimeString.length() == 0) { return System.currentTimeMillis(); } if (this.dateTimeFormatter == null) { if (dateTimeString.length() < 19 || dateTimeString.charAt(10) != 'T') { return System.currentTimeMillis(); } else { try { return parseRFC3339Date(dateTimeString).getTime(); } catch (Exception e) { return System.currentTimeMillis(); // we gave it a shot, but it still didn't parse properly } } } try { Date date = this.dateTimeFormatter.parse(dateTimeString); Calendar cal = Calendar.getInstance(); cal.setTime(date); if (cal.get(Calendar.YEAR) < 2000) { // the log message probably didn't have the year in it, set it to our current year cal.set(Calendar.YEAR, Calendar.getInstance().get(Calendar.YEAR)); } return cal.getTimeInMillis(); } catch (Exception e) { if (log.isTraceEnabled()) { log.trace("Failed to parse date/time [" + dateTimeString + "] with format [" + this.dateTimeFormatter.toPattern() + "]. " + e); } return System.currentTimeMillis(); } } /** * Given a severity string, returns the severity enum. * * @param severityString * * @return enum */ protected EventSeverity getSeverity(String severityString) { EventSeverity severity = EventSeverity.INFO; // if we don't know it, flag it as INFO if (severityString != null) { severityString = severityString.toUpperCase(); if (severityString.startsWith("EMERG") || severityString.startsWith("CRIT")) { severity = EventSeverity.FATAL; } else if (severityString.startsWith("ERR")) { severity = EventSeverity.ERROR; } else if (severityString.startsWith("WARN")) { severity = EventSeverity.WARN; } else if (severityString.startsWith("NOTICE") || severityString.startsWith("INFO")) { severity = EventSeverity.INFO; } else if (severityString.startsWith("DEBUG")) { severity = EventSeverity.DEBUG; } } return severity; } /** * Parses RFC3339 dates, but assumes the given RFC3339 represents the same timezone * as our default (which it should be, the syslog entry is on the same box as we are). * This makes the parsing easier and quicker. If the string cannot be parsed, * the current time is returned. */ private static SimpleDateFormat RFC3339_FORMAT_ZULU; private static SimpleDateFormat RFC3339_FORMAT; static { try { RFC3339_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); RFC3339_FORMAT_ZULU = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); RFC3339_FORMAT_ZULU.setTimeZone(TimeZone.getTimeZone("UTC")); } catch (Throwable t) { LogFactory.getLog(SyslogProcessor.class).warn("Cannot setup rfc3999 formats", t); } } protected Date parseRFC3339Date(String rfc3339String) throws Exception { // we will truncate the millis since there seems to be a VM bug or at least odd behavior when parsing millis. // also need to remove the : from the timezone offset if it exists, SimpleDateFormat doesn't like it // end up with the format: yyyy-MM-ddTHH:mm:ss(+|-)#### rfc3339String = rfc3339String.replaceFirst("\\.\\d*", ""); // remove millis // if there is no timezone, parse it as if in zulu timezone if (rfc3339String.endsWith("Z")) { Date date = RFC3339_FORMAT_ZULU.parse(rfc3339String); return date; } // remove any colon found in timezone and parse it if (rfc3339String.matches(".*[\\+\\-]\\d\\d:\\d\\d.*")) { int length = rfc3339String.length(); rfc3339String = rfc3339String.substring(0, length - 3) + rfc3339String.substring(length - 2); } Date date = RFC3339_FORMAT.parse(rfc3339String); return date; } }