/*
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2013 - 2014 Adobe
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.adobe.acs.commons.logging.impl;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Properties;
import org.apache.jackrabbit.util.ISO8601;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Logs OSGi Events for any set of topics to an SLF4j Logger Category, as JSON objects.
*/
@Component(metatype = true, configurationFactory = true, policy = ConfigurationPolicy.REQUIRE,
label = "ACS AEM Commons - JSON Event Logger", description = "Logs OSGi Events for any set of topics to an SLF4j Logger Category, as JSON objects.")
@SuppressWarnings("PMD.MoreThanOneLogger")
@Properties({
@Property(
name = "webconsole.configurationFactory.nameHint",
value = "Logger: {event.logger.category} for events matching '{event.filter}' on '{event.topics}'")
})
public class JsonEventLogger implements EventHandler {
/**
* Use this logger for tracing this service instance's own lifecycle.
*/
private static final Logger log = LoggerFactory.getLogger(JsonEventLogger.class);
/** We add this timestamp property to all logged events */
private static final String PROP_TIMESTAMP = "_timestamp";
private static final String DEFAULT_LEVEL = "INFO";
/**
* A simple enum for Slf4j logging levels.
*/
private enum LogLevel {
TRACE, DEBUG, INFO, WARN, ERROR;
public static LogLevel fromProperty(String prop) {
if (prop != null) {
for (LogLevel value : values()) {
if (value.name().equalsIgnoreCase(prop)) {
return value;
}
}
}
return null;
}
}
@Property(label = "Event Topics", unbounded = PropertyUnbounded.ARRAY,
description = "This value lists the topics handled by this logger. The value is a list of strings. If the string ends with a star, all topics in this package and all subpackages match. If the string does not end with a star, this is assumed to define an exact topic.")
private static final String OSGI_TOPICS = EventConstants.EVENT_TOPIC;
@Property(label = "Event Filter", description = "LDAP-style event filter query. Leave blank to log all events to the configured topic or topics.")
private static final String OSGI_FILTER = EventConstants.EVENT_FILTER;
@Property(label = "Logger Name", description = "The Sling SLF4j Logger Name or Category to send the JSON messages to. Leave empty to disable the logger.")
private static final String OSGI_CATEGORY = "event.logger.category";
@Property(label = "Logger Level", value = DEFAULT_LEVEL, options = {
@PropertyOption(name = "TRACE", value = "Trace"),
@PropertyOption(name = "DEBUG", value = "Debug"),
@PropertyOption(name = "INFO", value = "Information"),
@PropertyOption(name = "WARN", value = "Warnings"),
@PropertyOption(name = "ERROR", value = "Error")
}, description = "Select the logging level the messages should be sent with.")
private static final String OSGI_LEVEL = "event.logger.level";
private String[] topics;
private String filter;
private String category;
private String level;
private boolean valid;
/**
* Suppress the PMD.LoggerIsNotStaticFinal check because the point is to have an
* SCR-configurable logger separate from the normal class-level log object defined
* above.
*/
@SuppressWarnings("PMD.LoggerIsNotStaticFinal")
private Logger eventLogger;
private LogLevel logLevel;
private ServiceRegistration registration;
/**
* Writes an event to the configured logger using the configured log level
* @param event an OSGi Event
*/
private void logEvent(Event event) {
log.trace("[logEvent] event={}", event);
try {
String message = constructMessage(event);
if (logLevel == LogLevel.ERROR) {
this.eventLogger.error(message);
} else if (logLevel == LogLevel.WARN) {
this.eventLogger.warn(message);
} else if (logLevel == LogLevel.INFO) {
this.eventLogger.info(message);
} else if (logLevel == LogLevel.DEBUG) {
this.eventLogger.debug(message);
} else if (logLevel == LogLevel.TRACE) {
this.eventLogger.trace(message);
}
} catch (JSONException e) {
log.error("[logEvent] failed to construct log message from event: " + event.toString(), e);
}
}
/**
* Determines if the logger category is enabled at the configured level
* @return true if the logger is enabled at the configured log level
*/
private boolean isLoggerEnabled() {
if (this.eventLogger != null && this.logLevel != null) {
if (logLevel == LogLevel.ERROR) {
return this.eventLogger.isErrorEnabled();
} else if (logLevel == LogLevel.WARN) {
return this.eventLogger.isWarnEnabled();
} else if (logLevel == LogLevel.INFO) {
return this.eventLogger.isInfoEnabled();
} else if (logLevel == LogLevel.DEBUG) {
return this.eventLogger.isDebugEnabled();
} else if (logLevel == LogLevel.TRACE) {
return this.eventLogger.isTraceEnabled();
}
}
return false;
}
/**
* Serializes an OSGi {@link org.osgi.service.event.Event} into a JSON object string
*
* @param event the event to be serialized as
* @return a serialized JSON object
* @throws org.apache.sling.commons.json.JSONException
*/
protected static String constructMessage(Event event) throws JSONException {
JSONObject obj = new JSONObject();
for (String prop : event.getPropertyNames()) {
Object val = event.getProperty(prop);
Object converted = convertValue(val);
obj.put(prop, converted == null ? val : converted);
}
obj.put(PROP_TIMESTAMP, ISO8601.format(Calendar.getInstance()));
return obj.toString();
}
/**
* Converts individual java objects to JSONObjects using reflection and recursion
* @param val an untyped Java object to try to convert
* @return {@code val} if not handled, or return a converted JSONObject, JSONArray, or String
* @throws JSONException
*/
@SuppressWarnings("unchecked")
protected static Object convertValue(Object val) throws JSONException {
if (val.getClass().isArray()) {
Object[] vals = (Object[]) val;
JSONArray array = new JSONArray();
for (Object arrayVal : vals) {
Object converted = convertValue(arrayVal);
array.put(converted == null ? arrayVal : converted);
}
return array;
} else if (val instanceof Collection) {
JSONArray array = new JSONArray();
for (Object arrayVal : (Collection<?>) val) {
Object converted = convertValue(arrayVal);
array.put(converted == null ? arrayVal : converted);
}
return array;
} else if (val instanceof Map) {
Map<?, ?> valMap = (Map<?, ?>) val;
JSONObject obj = new JSONObject();
if (valMap.isEmpty()) {
return obj;
} else if (valMap.keySet().iterator().next() instanceof String) {
for (Map.Entry<String, ?> entry : ((Map<String, ?>) valMap).entrySet()) {
Object converted = convertValue(entry.getValue());
obj.put(entry.getKey(), converted == null ? entry.getValue() : converted);
}
} else {
for (Map.Entry<?, ?> entry : valMap.entrySet()) {
Object converted = convertValue(entry.getValue());
obj.put(entry.getKey().toString(),
converted == null ? entry.getValue() : converted);
}
}
return obj;
} else if (val instanceof Calendar) {
try {
return ISO8601.format((Calendar) val);
} catch (IllegalArgumentException e) {
log.debug("[constructMessage] failed to convert Calendar to ISO8601 String: {}, {}", e.getMessage(), val);
}
} else if (val instanceof Date) {
try {
Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) val);
return ISO8601.format(calendar);
} catch (IllegalArgumentException e) {
log.debug("[constructMessage] failed to convert Date to ISO8601 String: {}, {}", e.getMessage(), val);
}
}
return val;
}
//
// ---------------------------------------------------------< EventHandler methods >-----
//
/**
* {@inheritDoc}
*/
@Override
public void handleEvent(Event event) {
if (event.getProperty("event.application") == null && this.isLoggerEnabled()) {
logEvent(event);
}
}
//
// ---------------------------------------------------------< SCR methods >-------------
//
@Activate
protected void activate(ComponentContext ctx) {
log.trace("[activate] entered activate method.");
Dictionary<?, ?> props = ctx.getProperties();
this.topics = PropertiesUtil.toStringArray(props.get(OSGI_TOPICS));
this.filter = PropertiesUtil.toString(props.get(OSGI_FILTER), "").trim();
this.category = PropertiesUtil.toString(props.get(OSGI_CATEGORY), "").trim();
this.level = PropertiesUtil.toString(props.get(OSGI_LEVEL), DEFAULT_LEVEL);
this.logLevel = LogLevel.fromProperty(this.level);
this.valid = (this.topics != null && this.topics.length > 0 && !this.category.isEmpty());
if (this.valid) {
this.eventLogger = LoggerFactory.getLogger(this.category);
Dictionary<String, Object> registrationProps = new Hashtable<String, Object>();
registrationProps.put(EventConstants.EVENT_TOPIC, this.topics);
if (!this.filter.isEmpty()) {
registrationProps.put(EventConstants.EVENT_FILTER, this.filter);
}
this.registration = ctx.getBundleContext().registerService(EventHandler.class.getName(), this, registrationProps);
} else {
log.warn("Not registering invalid event handler. Check configuration.");
}
log.debug("[activate] logger state: {}", this);
}
@Deactivate
protected void deactivate() {
log.trace("[deactivate] entered deactivate method.");
if (this.registration != null) {
this.registration.unregister();
this.registration = null;
}
this.eventLogger = null;
this.logLevel = null;
}
//
// ---------------------------------------------------------< Object methods >-------------
//
@Override
public String toString() {
return "EventLogger{" +
"valid=" + valid +
", topics=" + Arrays.toString(topics) +
", filter='" + filter + '\'' +
", category='" + category + '\'' +
", level='" + level + '\'' +
", enabled=" + isLoggerEnabled() +
'}';
}
}