configuration webservice for MARMOTTA-390 (configurable logging) functional
Project: http://git-wip-us.apache.org/repos/asf/marmotta/repo Commit: http://git-wip-us.apache.org/repos/asf/marmotta/commit/e5dd9cf6 Tree: http://git-wip-us.apache.org/repos/asf/marmotta/tree/e5dd9cf6 Diff: http://git-wip-us.apache.org/repos/asf/marmotta/diff/e5dd9cf6 Branch: refs/heads/develop Commit: e5dd9cf63a7f3fe4526aa7a09677ab1125f65ecc Parents: 7b9cb5a Author: Sebastian Schaffert <[email protected]> Authored: Thu Nov 28 16:24:25 2013 +0100 Committer: Sebastian Schaffert <[email protected]> Committed: Thu Nov 28 16:24:25 2013 +0100 ---------------------------------------------------------------------- .../core/api/config/ConfigurationService.java | 2 + .../config/ConfigurationServiceImpl.java | 98 +++++- .../services/logging/LoggingServiceImpl.java | 9 + .../webservices/logging/LoggingWebService.java | 332 +++++++++++++++++++ .../src/main/resources/kiwi-module.properties | 3 +- 5 files changed, 426 insertions(+), 18 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/marmotta/blob/e5dd9cf6/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/api/config/ConfigurationService.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/api/config/ConfigurationService.java b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/api/config/ConfigurationService.java index 4f8c5f1..19909c4 100644 --- a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/api/config/ConfigurationService.java +++ b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/api/config/ConfigurationService.java @@ -69,6 +69,8 @@ public interface ConfigurationService { static final String DIR_IMPORT = "import"; + static final String LOGGING_PATH = "logging"; + /** * Get the base URI of the system. * The base URI is used by the LMF to create local resource URIs. In this way, all Apache Marmotta resources http://git-wip-us.apache.org/repos/asf/marmotta/blob/e5dd9cf6/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/config/ConfigurationServiceImpl.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/config/ConfigurationServiceImpl.java b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/config/ConfigurationServiceImpl.java index 676f763..752fe74 100644 --- a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/config/ConfigurationServiceImpl.java +++ b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/config/ConfigurationServiceImpl.java @@ -26,6 +26,7 @@ import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.MapConfiguration; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.marmotta.platform.core.api.config.ConfigurationService; import org.apache.marmotta.platform.core.events.ConfigurationChangedEvent; import org.apache.marmotta.platform.core.events.ConfigurationServiceInitEvent; @@ -48,15 +49,9 @@ import java.io.IOException; import java.lang.reflect.Array; import java.net.URL; import java.net.UnknownHostException; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -128,10 +123,28 @@ public class ConfigurationServiceImpl implements ConfigurationService { */ private ReadWriteLock lock; + /** + * Backlog for delayed event collection; only fires a configuration changed event if there has not been a further + * update in a specified amount of time (default 250ms); + */ + private Set<String> eventBacklog; + + private long EVENT_DELAY = 250L; + + + /* + * Timer and task for delayed execution of configuration changed events + */ + private Timer eventTimer; + private ReentrantLock eventLock; + public ConfigurationServiceImpl() { runtimeFlags = new HashMap<String, Boolean>(); lock = new ReentrantReadWriteLock(); + + eventTimer = new Timer("Configuration Event Timer", true); + eventLock = new ReentrantLock(); } /** @@ -578,7 +591,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { } if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -713,7 +726,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { } if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -764,7 +777,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { } if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -831,7 +844,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { } if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -886,7 +899,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -982,7 +995,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { if (!initialising) { log.debug("firing configuration changed event for key {}", key); - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -1030,7 +1043,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { if (!initialising) { log.debug("firing configuration changed event for key {}", key); - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -1087,7 +1100,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(values.keySet())); + raiseDelayedConfigurationEvent((values.keySet())); } } @@ -1107,7 +1120,7 @@ public class ConfigurationServiceImpl implements ConfigurationService { if (!initialising) { - configurationEvent.fire(new ConfigurationChangedEvent(key)); + raiseDelayedConfigurationEvent(Collections.singleton(key)); } } } @@ -1470,4 +1483,55 @@ public class ConfigurationServiceImpl implements ConfigurationService { } return value; } + + + /** + * Start a delayed execution of raising an event + * @param keys + */ + private void raiseDelayedConfigurationEvent(Set<String> keys) { + eventLock.lock(); + try { + if(eventBacklog == null) { + eventBacklog = new HashSet<>(); + } + eventBacklog.addAll(keys); + + if(eventTimer != null) { + eventTimer.cancel(); + } + eventTimer = new Timer("Configuration Event Timer", true); + eventTimer.schedule(new EventTimerTask(), EVENT_DELAY); + } finally { + eventLock.unlock(); + } + + if(log.isDebugEnabled()) { + log.debug("updated configuration keys [{}]", StringUtils.join(keys,", ")); + } + + } + + /** + * Delayed event firing task + */ + private class EventTimerTask extends TimerTask { + + @Override + public void run() { + eventLock.lock(); + try { + Set<String> keys = eventBacklog; + eventBacklog = null; + + if(log.isDebugEnabled()) { + log.debug("firing delayed ({}ms) configuration changed event with keys [{}]",EVENT_DELAY, StringUtils.join(keys,", ")); + } + + configurationEvent.fire(new ConfigurationChangedEvent(keys)); + } finally { + eventLock.unlock(); + } + } + } } http://git-wip-us.apache.org/repos/asf/marmotta/blob/e5dd9cf6/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/logging/LoggingServiceImpl.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/logging/LoggingServiceImpl.java b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/logging/LoggingServiceImpl.java index d77897b..0265660 100644 --- a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/logging/LoggingServiceImpl.java +++ b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/services/logging/LoggingServiceImpl.java @@ -38,6 +38,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.marmotta.platform.core.api.config.ConfigurationService; import org.apache.marmotta.platform.core.api.logging.LoggingModule; import org.apache.marmotta.platform.core.api.logging.LoggingService; +import org.apache.marmotta.platform.core.events.ConfigurationChangedEvent; import org.apache.marmotta.platform.core.events.LoggingStartEvent; import org.apache.marmotta.platform.core.exception.MarmottaConfigurationException; import org.apache.marmotta.platform.core.model.logging.ConsoleOutput; @@ -150,6 +151,14 @@ public class LoggingServiceImpl implements LoggingService { configureLoggers(); } + public void configurationEventHandler(@Observes ConfigurationChangedEvent event) { + if(event.containsChangedKeyWithPrefix("logging.")) { + log.warn("LOGGING: Reloading logging configuration"); + + configureLoggers(); + } + } + /** * Configure all loggers according to their configuration and set some reasonable fallback for the root level http://git-wip-us.apache.org/repos/asf/marmotta/blob/e5dd9cf6/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/webservices/logging/LoggingWebService.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/webservices/logging/LoggingWebService.java b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/webservices/logging/LoggingWebService.java new file mode 100644 index 0000000..c44ab09 --- /dev/null +++ b/platform/marmotta-core/src/main/java/org/apache/marmotta/platform/core/webservices/logging/LoggingWebService.java @@ -0,0 +1,332 @@ +package org.apache.marmotta.platform.core.webservices.logging; + +import ch.qos.logback.classic.Level; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; +import org.apache.marmotta.platform.core.api.config.ConfigurationService; +import org.apache.marmotta.platform.core.api.logging.LoggingModule; +import org.apache.marmotta.platform.core.api.logging.LoggingService; +import org.apache.marmotta.platform.core.model.logging.ConsoleOutput; +import org.apache.marmotta.platform.core.model.logging.LogFileOutput; +import org.apache.marmotta.platform.core.model.logging.LoggingOutput; +import org.apache.marmotta.platform.core.model.logging.SyslogOutput; +import org.codehaus.jackson.JsonParseException; +import org.codehaus.jackson.map.JsonMappingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Listing and modifying logging configuration for appenders and logging modules. JSON format for each appender is: + * <pre> + * { + * "type": ("console" | "logfile" | "syslog" ), + * "id": unique identifier + * "name": human-readable name + * "pattern": pattern for logging output + * "level": ("ERROR" | "WARN" | "INFO" | "DEBUG" | "TRACE" | "OFF" ) + * } + * </pre> + * + * Depending on the type of appender, the following additional fields need to be provided: + * <ul> + * <li><strong>logfile</strong> + * <pre> + * "file": name of the logfile + * "keep": how many days to keep old logfiles + * </pre> + * </li> + * <li><strong>syslog</strong> + * <pre> + * "host": name of server where the syslog daemon is running + * "facility": facility to use for logging + * </pre> + * </li> + * </ul> + * + * JSON format for logging modules is: + * <pre> + * { + * "id": unique identifier + * "name": human-readable name + * "level": ("ERROR" | "WARN" | "INFO" | "DEBUG" | "TRACE" | "OFF" ) + * "appenders": list of appender ids to send logging to + * "packages": list of package ids managed by module + * } + * </pre> + * + * @author Sebastian Schaffert ([email protected]) + */ +@ApplicationScoped +@Path("/" + ConfigurationService.LOGGING_PATH) +public class LoggingWebService { + + @Inject + private ConfigurationService configurationService; + + @Inject + private LoggingService loggingService; + + /** + * Get a JSON list of all log appenders currently configured in the system using the JSON format described in the + * header of the class + * + * @return JSON list + */ + @GET + @Path("/appenders") + @Produces("application/json") + public Response listAppenders() { + return Response.ok(Lists.transform(loggingService.listOutputConfigurations(), new Function<LoggingOutput, Object>() { + @Override + public Object apply(LoggingOutput input) { + return appenderToJSON(input); + } + })).build(); + } + + /** + * Get the configuration of the log appender with the given ID using the JSON format described in the header of + * the class + * + * @HTTP 200 appender configuration returned successfully + * @HTTP 404 appender not found + * + * @param id unique identifier of appender + * @return JSON formatted representation of configuration + */ + @GET + @Path("/appenders/{id}") + @Produces("application/json") + public Response getAppender(@PathParam("id") String id) { + LoggingOutput appender = loggingService.getOutputConfiguration(id); + if(appender != null) { + return Response.ok(appenderToJSON(appender)).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + + /** + * Create or update the appender with the given id, using the JSON description sent in the body of the request + * + * @HTTP 200 appender updated successfully + * @HTTP 400 appender configuration invalid (e.g. not proper JSON) + * @HTTP 404 appender not found + * + * @param id unique identifier of appender + * @return HTTP status 200 in case of success + */ + @POST + @Path("/appenders/{id}") + @Consumes("application/json") + public Response updateAppender(@PathParam("id") String id, @Context HttpServletRequest request) { + ObjectMapper mapper = new ObjectMapper(); + try { + //log.info(getContentData(request.getReader())); + Map<String,Object> values = mapper.readValue(request.getInputStream(), new TypeReference<HashMap<String,Object>>(){}); + + String type = (String) values.get("type"); + String name = (String) values.get("name"); + String level = (String) values.get("level"); + String pattern = (String) values.get("pattern"); + + LoggingOutput appender = loggingService.getOutputConfiguration(id); + if(appender == null) { + // type information required + Preconditions.checkArgument(type != null, "appender type was not given"); + Preconditions.checkArgument(name != null, "appender name was not given"); + + if("logfile".equals(type)) { + String file = (String) values.get("file"); + + Preconditions.checkArgument(file != null, "logfile name was not given"); + + appender = loggingService.createLogFileOutput(id,name,file); + } else if("syslog".equals(type)) { + String host = (String) values.get("host"); + + Preconditions.checkArgument(host != null, "syslog host was not given"); + + appender = loggingService.createSyslogOutput(id,name); + } else { + return Response.status(Response.Status.NOT_IMPLEMENTED).entity("new appenders of type "+type+" not supported").build(); + } + } + + appender.setName(name); + + if(level != null) { + appender.setMaxLevel(Level.toLevel(level)); + } + if(pattern != null) { + appender.setPattern(pattern); + } + if(values.get("file") != null && appender instanceof LogFileOutput) { + ((LogFileOutput) appender).setFileName((String) values.get("file")); + } + if(values.get("keep") != null && appender instanceof LogFileOutput) { + ((LogFileOutput) appender).setKeepDays(Integer.parseInt(values.get("keep").toString())); + } + if(values.get("host") != null && appender instanceof SyslogOutput) { + ((SyslogOutput) appender).setHostName((String) values.get("host")); + } + if(values.get("facility") != null && appender instanceof SyslogOutput) { + ((SyslogOutput) appender).setFacility((String) values.get("facility")); + } + + return Response.ok().build(); + } catch (IllegalArgumentException ex) { + // thrown by Preconditions.checkArgument + return Response.status(Response.Status.BAD_REQUEST).entity(ex.getMessage()).build(); + } catch (JsonMappingException | JsonParseException e) { + return Response.status(Response.Status.BAD_REQUEST).entity("invalid JSON format: "+e.getMessage()).build(); + } catch (IOException e) { + return Response.status(Response.Status.BAD_REQUEST).entity("could not read stream: "+e.getMessage()).build(); + } + } + + + /** + * List all modules currently available in the system as a JSON list using the JSON format described in the + * header of this class + * + * @HTTP 200 in case the modules are listed properly + * + * @return JSON list of module descriptions + */ + @GET + @Path("/modules") + @Produces("application/json") + public Response listModules() { + return Response.ok(Lists.transform(loggingService.listModules(), new Function<LoggingModule, Object>() { + @Override + public Object apply(LoggingModule input) { + return moduleToJSON(input); + } + })).build(); + } + + /** + * Get the configuration of the logging module with the given id, using the JSON format described in the + * header of this class. + * + * @HTTP 200 module found + * @HTTP 404 module not found + * + * @param id unique logging module identifier + * @return HTTP status 200 in case of success + */ + @GET + @Path("/modules/{id}") + @Produces("application/json") + public Response getModule(@PathParam("id") String id) { + for(LoggingModule module : loggingService.listModules()) { + if(StringUtils.equals(module.getId(), id)) { + return Response.ok(moduleToJSON(module)).build(); + } + } + + return Response.status(Response.Status.NOT_FOUND).build(); + } + + + + /** + * Update the module with the given id, using the JSON description sent in the body of the request. Only the fields + * "level" and "appenders" can be updated for modules. + * + * @HTTP 200 module updated successfully + * @HTTP 400 module configuration invalid (e.g. not proper JSON) + * @HTTP 404 module not found + * + * @param id unique logging module identifier + * @return HTTP status 200 in case of success + */ + @POST + @Path("/modules/{id}") + @Consumes("application/json") + public Response updateModule(@PathParam("id") String id, @Context HttpServletRequest request) { + ObjectMapper mapper = new ObjectMapper(); + try { + //log.info(getContentData(request.getReader())); + Map<String,Object> values = mapper.readValue(request.getInputStream(), new TypeReference<HashMap<String,Object>>(){}); + + String level = (String) values.get("level"); + List appenders = (List) values.get("appenders"); + for(LoggingModule module : loggingService.listModules()) { + if(StringUtils.equals(module.getId(), id)) { + if(level != null) { + module.setCurrentLevel(Level.toLevel(level)); + } + if(appenders != null) { + module.setLoggingOutputIds(appenders); + } + + return Response.ok("module updated").build(); + } + } + + return Response.status(Response.Status.NOT_FOUND).build(); + } catch (JsonMappingException | JsonParseException e) { + return Response.status(Response.Status.BAD_REQUEST).entity("invalid JSON format: "+e.getMessage()).build(); + } catch (IOException e) { + return Response.status(Response.Status.BAD_REQUEST).entity("could not read stream: "+e.getMessage()).build(); + } + } + + + private static Map<String,Object> appenderToJSON(LoggingOutput out) { + Map<String,Object> result = new HashMap<>(); + + if(out instanceof SyslogOutput) { + result.put("type", "syslog"); + result.put("host", ((SyslogOutput) out).getHostName()); + result.put("facility", ((SyslogOutput) out).getFacility()); + } else if(out instanceof ConsoleOutput) { + result.put("type", "console"); + } else if(out instanceof LogFileOutput) { + result.put("type", "logfile"); + result.put("file", ((LogFileOutput) out).getFileName()); + result.put("keep", ((LogFileOutput) out).getKeepDays()); + } + + result.put("level", out.getMaxLevel().toString()); + result.put("pattern", out.getPattern()); + result.put("name", out.getName()); + result.put("id", out.getId()); + + return result; + } + + + private static Map<String,Object> moduleToJSON(LoggingModule module) { + Map<String,Object> result = new HashMap<>(); + + result.put("id", module.getId()); + result.put("name", module.getName()); + result.put("level", module.getCurrentLevel().toString()); + result.put("appenders", module.getLoggingOutputIds()); + result.put("packages", module.getPackages()); + + return result; + } +} http://git-wip-us.apache.org/repos/asf/marmotta/blob/e5dd9cf6/platform/marmotta-core/src/main/resources/kiwi-module.properties ---------------------------------------------------------------------- diff --git a/platform/marmotta-core/src/main/resources/kiwi-module.properties b/platform/marmotta-core/src/main/resources/kiwi-module.properties index aeecd4d..a71bb0b 100644 --- a/platform/marmotta-core/src/main/resources/kiwi-module.properties +++ b/platform/marmotta-core/src/main/resources/kiwi-module.properties @@ -70,5 +70,6 @@ webservices=org.apache.marmotta.platform.core.webservices.config.ConfigurationWe org.apache.marmotta.platform.core.webservices.resource.InspectionWebService,\ org.apache.marmotta.platform.core.webservices.triplestore.LdpWebService,\ org.apache.marmotta.platform.core.webservices.triplestore.ContextWebService,\ - org.apache.marmotta.platform.core.webservices.prefix.PrefixWebService + org.apache.marmotta.platform.core.webservices.prefix.PrefixWebService,\ + org.apache.marmotta.platform.core.webservices.logging.LoggingWebService \ No newline at end of file
