/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.io.rest.log.internal;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.smarthome.io.rest.RESTResource;
import org.eclipse.smarthome.io.rest.log.LogConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
@Path("/log")
@Api(value = LogHandler.PATH_LOG)
@Produces(MediaType.APPLICATION_JSON)
public class LogHandler implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(LogHandler.class);
public static final String PATH_LOG = "log";
private final static String TEMPLATE_INTERNAL_ERROR = "{\"error\":\"%s\",\"severity\":\"%s\"}";
/**
* Rolling array to store the last LOG_BUFFER_LIMIT messages. Those can be fetched e.g. by a
* diagnostic UI to display errors of other clients, where e.g. the logs are not easily accessible.
*/
private final ConcurrentLinkedDeque<LogMessage> LOG_BUFFER = new ConcurrentLinkedDeque<>();
/**
* Container for a log message
*/
public class LogMessage {
public long timestamp;
public String severity;
public URL url;
public String message;
}
@GET
@Path("/levels")
@ApiOperation(value = "Get log severities, which are logged by the current logger settings.", code = 200, notes = "This depends on the current log settings at the backend.")
public Response getLogLevels() {
return Response.ok(createLogLevelsMap()).build();
}
@GET
@ApiOperation(value = "Returns the last logged frontend messages. The amount is limited to the "
+ LogConstants.LOG_BUFFER_LIMIT + " last entries.")
@ApiParam(name = "limit", allowableValues = "range[1, " + LogConstants.LOG_BUFFER_LIMIT + "]")
public Response getLastLogs(@DefaultValue(LogConstants.LOG_BUFFER_LIMIT + "") @QueryParam("limit") Integer limit) {
if (LOG_BUFFER.size() <= 0) {
return Response.ok("[]").build();
}
if (limit == null || limit <= 0 || limit > LogConstants.LOG_BUFFER_LIMIT) {
limit = LOG_BUFFER.size();
}
if (limit >= LOG_BUFFER.size()) {
return Response.ok(LOG_BUFFER.toArray()).build();
} else {
final List<LogMessage> result = new ArrayList<>();
Iterator<LogMessage> iter = LOG_BUFFER.descendingIterator();
do {
result.add(iter.next());
} while (iter.hasNext() && result.size() < limit);
Collections.reverse(result);
return Response.ok(result).build();
}
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Log a frontend log message to the backend.")
@ApiParam(name = "logMessage", value = "Severity is required and can be one of error, warn, info or debug, depending on activated severities which you can GET at /logLevels.", example = "{\"severity\": \"error\", \"url\": \"http://example.org\", \"message\": \"Error message\"}")
@ApiResponses({ @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = LogConstants.LOG_SEVERITY_IS_NOT_SUPPORTED) })
public Response log(final LogMessage logMessage) {
if (logMessage == null) {
logger.debug("Received null log message model!");
return Response.status(500).entity(String.format(TEMPLATE_INTERNAL_ERROR, LogConstants.LOG_HANDLE_ERROR))
.build();
}
logMessage.timestamp = Calendar.getInstance().getTimeInMillis();
if (!doLog(logMessage)) {
return Response.status(403).entity(String.format(TEMPLATE_INTERNAL_ERROR,
LogConstants.LOG_SEVERITY_IS_NOT_SUPPORTED, logMessage.severity)).build();
}
LOG_BUFFER.add(logMessage);
if (LOG_BUFFER.size() > LogConstants.LOG_BUFFER_LIMIT) {
LOG_BUFFER.pollLast(); // Remove last element of Deque
}
return Response.ok().build();
}
/**
* Executes the logging call.
*
* @param logMessage
* @return Falls if severity is not supported, true if successfully logged.
*/
private boolean doLog(LogMessage logMessage) {
switch (logMessage.severity.toLowerCase()) {
case "error":
logger.error(LogConstants.FRONTEND_LOG_PATTERN, logMessage.url, logMessage.message);
break;
case "warn":
logger.warn(LogConstants.FRONTEND_LOG_PATTERN, logMessage.url, logMessage.message);
break;
case "info":
logger.info(LogConstants.FRONTEND_LOG_PATTERN, logMessage.url, logMessage.message);
break;
case "debug":
logger.debug(LogConstants.FRONTEND_LOG_PATTERN, logMessage.url, logMessage.message);
break;
default:
return false;
}
return true;
}
/**
* Return map of currently logged messages. They can change at runtime.
*/
private Map<String, Boolean> createLogLevelsMap() {
Map<String, Boolean> result = new HashMap<>();
result.put("error", logger.isErrorEnabled());
result.put("warn", logger.isWarnEnabled());
result.put("info", logger.isInfoEnabled());
result.put("debug", logger.isDebugEnabled());
return result;
}
}