package uws.service.log;
/*
* This file is part of UWSLibrary.
*
* UWSLibrary is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* UWSLibrary 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2012-2016 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
* Astronomisches Rechen Institut (ARI)
*/
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import uws.UWSException;
import uws.UWSToolBox;
import uws.job.UWSJob;
import uws.job.user.JobOwner;
import uws.service.UWS;
import uws.service.file.UWSFileManager;
/**
* <p>Default implementation of {@link UWSLog} interface which lets logging any message about a UWS.</p>
*
* @author Grégory Mantelet (CDS;ARI)
* @version 4.2 (07/2016)
*/
public class DefaultUWSLog implements UWSLog {
/** Format to use to serialize all encountered dates. */
private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
protected final UWS uws;
protected final UWSFileManager fileManager;
protected final PrintWriter defaultOutput;
/** <p>Minimum level that a message must have in order to be logged.</p>
* <p>The default behavior is the following:</p>
* <ul>
* <li><b>DEBUG</b>: every messages are logged.</li>
* <li><b>INFO</b>: every messages EXCEPT DEBUG are logged.</li>
* <li><b>WARNING</b>: every messages EXCEPT DEBUG and INFO are logged.</li>
* <li><b>ERROR</b>: only ERROR and FATAL messages are logged.</li>
* <li><b>FATAL</b>: only FATAL messages are logged.</li>
* </ul>
* @since 4.1 */
protected LogLevel minLogLevel = LogLevel.DEBUG;
/**
* <p>Builds a {@link UWSLog} which will use the file manager
* of the given UWS to get the log output (see {@link UWSFileManager#getLogOutput(uws.service.log.UWSLog.LogLevel, String)}).</p>
*
* <p><i><u>note 1</u>: This constructor is particularly useful if the file manager of the given UWS may change.</i></p>
* <p><i><u>note 2</u>: If no output can be found in the file manager (or if there is no file manager),
* the standard error output ({@link System#err}) will be chosen automatically for all log messages.</i></p>
*
* @param uws A UWS.
*/
public DefaultUWSLog(final UWS uws){
this.uws = uws;
fileManager = null;
defaultOutput = null;
}
/**
* <p>Builds a {@link UWSLog} which will use the given file
* manager to get the log output (see {@link UWSFileManager#getLogOutput(uws.service.log.UWSLog.LogLevel, String)}).</p>
*
* <p><i><u>note 1</u>: This constructor is particularly useful if the way of managing log output may change in the given file manager.
* Indeed, the output may change in function of the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).</i></p>
*
* <p><i><u>note 2</u> If no output can be found in the file manager the standard error output ({@link System#err})
* will be chosen automatically for all log messages.</i></p>
*
* @param fm A UWS file manager.
*/
public DefaultUWSLog(final UWSFileManager fm){
uws = null;
fileManager = fm;
defaultOutput = null;
}
/**
* <p>Builds a {@link UWSLog} which will print all its
* messages into the given stream.</p>
*
* <p><i><u>note</u>: the given output will be used whatever is the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).</i></p>
*
* @param output An output stream.
*/
public DefaultUWSLog(final OutputStream output){
uws = null;
fileManager = null;
defaultOutput = new PrintWriter(output);
}
/**
* <p>Builds a {@link UWSLog} which will print all its
* messages into the given stream.</p>
*
* <p><i><u>note</u>: the given output will be used whatever is the type of message to log ({@link uws.service.log.UWSLog.LogLevel}).</i></p>
*
* @param writer A print writer.
*/
public DefaultUWSLog(final PrintWriter writer){
uws = null;
fileManager = null;
defaultOutput = writer;
}
/**
* <p>Get the minimum level that a message must have in order to be logged.</p>
*
* <p>The default behavior is the following:</p>
* <ul>
* <li><b>DEBUG</b>: every messages are logged.</li>
* <li><b>INFO</b>: every messages EXCEPT DEBUG are logged.</li>
* <li><b>WARNING</b>: every messages EXCEPT DEBUG and INFO are logged.</li>
* <li><b>ERROR</b>: only ERROR and FATAL messages are logged.</li>
* <li><b>FATAL</b>: only FATAL messages are logged.</li>
* </ul>
*
* @return The minimum log level.
*
* @since 4.1
*/
public final LogLevel getMinLogLevel(){
return minLogLevel;
}
/**
* <p>Set the minimum level that a message must have in order to be logged.</p>
*
* <p>The default behavior is the following:</p>
* <ul>
* <li><b>DEBUG</b>: every messages are logged.</li>
* <li><b>INFO</b>: every messages EXCEPT DEBUG are logged.</li>
* <li><b>WARNING</b>: every messages EXCEPT DEBUG and INFO are logged.</li>
* <li><b>ERROR</b>: only ERROR and FATAL messages are logged.</li>
* <li><b>FATAL</b>: only FATAL messages are logged.</li>
* </ul>
*
* <p><i>Note:
* If the given level is NULL, this function has no effect.
* </i></p>
*
* @param newMinLevel The new minimum log level.
*
* @since 4.1
*/
public final void setMinLogLevel(final LogLevel newMinLevel){
if (newMinLevel != null)
minLogLevel = newMinLevel;
}
/**
* Gets the date formatter/parser to use for any date read/write into this logger.
* @return A date formatter/parser.
*/
public final DateFormat getDateFormat(){
return dateFormat;
}
/**
* Sets the date formatter/parser to use for any date read/write into this logger.
* @param dateFormat The date formatter/parser to use from now. (MUST BE DIFFERENT FROM NULL)
*/
public final void setDateFormat(final DateFormat dateFormat){
if (dateFormat != null)
this.dateFormat = dateFormat;
}
/**
* <p>Gets an output for the given type of message to print.</p>
*
* <p>The {@link System#err} output is used if none can be found in the {@link UWS} or the {@link UWSFileManager}
* given at the creation, or if the given output stream or writer is NULL.</p>
*
* @param level Level of the message to print (DEBUG, INFO, WARNING, ERROR or FATAL).
* @param context Context of the message to print (UWS, HTTP, JOB, THREAD).
*
* @return A writer.
*/
protected PrintWriter getOutput(final LogLevel level, final String context){
try{
if (uws != null){
if (uws.getFileManager() != null)
return uws.getFileManager().getLogOutput(level, context);
}else if (fileManager != null)
return fileManager.getLogOutput(level, context);
else if (defaultOutput != null)
return defaultOutput;
}catch(IOException ioe){
ioe.printStackTrace(System.err);
}
return new PrintWriter(System.err);
}
/* *********************** */
/* GENERAL LOGGING METHODS */
/* *********************** */
/**
* <p>Normalize a log message.</p>
*
* <p>
* Since a log entry will a tab-separated concatenation of information, additional tabulations or new-lines
* would corrupt a log entry. This function replaces such characters by one space. Only \r are definitely deleted.
* </p>
*
* @param message Log message to normalize.
*
* @return The normalized log message.
*
* @since 4.1
*/
protected String normalizeMessage(final String message){
if (message == null)
return null;
else
return message.replaceAll("[\n\t]", " ").replaceAll("\r", "");
}
/**
* <p>Tells whether a message with the given error level can be logged or not.</p>
*
* <p>In function of the minimum log level of this class, the default behavior is the following:</p>
* <ul>
* <li><b>DEBUG</b>: every messages are logged.</li>
* <li><b>INFO</b>: every messages EXCEPT DEBUG are logged.</li>
* <li><b>WARNING</b>: every messages EXCEPT DEBUG and INFO are logged.</li>
* <li><b>ERROR</b>: only ERROR and FATAL messages are logged.</li>
* <li><b>FATAL</b>: only FATAL messages are logged.</li>
* </ul>
*
* @param msgLevel Level of the message which has been asked to log. <i>Note: if NULL, it will be considered as DEBUG.</i>
*
* @return <i>true</i> if the message associated with the given log level can be logged, <i>false</i> otherwise.
*
* @since 4.1
*/
protected boolean canLog(LogLevel msgLevel){
// No level specified => DEBUG
if (msgLevel == null)
msgLevel = LogLevel.DEBUG;
// Decide in function of the minimum log level set in this class:
switch(minLogLevel){
case INFO:
return (msgLevel != LogLevel.DEBUG);
case WARNING:
return (msgLevel != LogLevel.DEBUG && msgLevel != LogLevel.INFO);
case ERROR:
return (msgLevel == LogLevel.ERROR || msgLevel == LogLevel.FATAL);
case FATAL:
return (msgLevel == LogLevel.FATAL);
case DEBUG:
default:
return true;
}
}
@Override
public void log(LogLevel level, final String context, final String message, final Throwable error){
log(level, context, null, null, message, null, error);
}
/**
* <p>Logs a full message and/or error.</p>
*
* <p><i>Note:
* If no message and error is provided, nothing will be written.
* </i></p>
*
* @param level Level of the error (DEBUG, INFO, WARNING, ERROR, FATAL). <i>SHOULD NOT be NULL</i>
* @param context Context of the error (UWS, HTTP, THREAD, JOB). <i>MAY be NULL</i>
* @param event Context event during which this log is emitted. <i>MAY be NULL</i>
* @param ID ID of the job or HTTP request (it may also be an ID of anything else). <i>MAY BE NULL</i>
* @param message Message of the error. <i>MAY be NULL</i>
* @param addColumn Additional column to append after the message and before the stack trace.
* @param error Error at the origin of the log error/warning/fatal. <i>MAY be NULL</i>
*
* @since 4.1
*/
protected final void log(LogLevel level, final String context, final String event, final String ID, final String message, final String addColumn, final Throwable error){
// If no message and no error is provided, nothing to log, so nothing to write:
if ((message == null || message.length() <= 0) && error == null)
return;
// If the type is missing:
if (level == null)
level = (error != null) ? LogLevel.ERROR : LogLevel.INFO;
// Log or not?
if (!canLog(level))
return;
StringBuffer buf = new StringBuffer();
// Print the date/time:
buf.append(dateFormat.format(new Date())).append('\t');
// Print the level of error (debug, info, warning, error, fatal):
buf.append(level.toString()).append('\t');
// Print the context of the error (uws, thread, job, http):
buf.append((context == null) ? "" : context).append('\t');
// Print the context event:
buf.append((event == null) ? "" : event).append('\t');
// Print an ID (jobID, requestID):
buf.append((ID == null) ? "" : ID).append('\t');
// Print the message:
if (message != null)
buf.append(normalizeMessage(message));
else if (error != null)
buf.append("[EXCEPTION ").append(error.getClass().getName()).append("] ").append(normalizeMessage(error.getMessage()));
// Print the additional column, if any:
if (addColumn != null)
buf.append('\t').append(normalizeMessage(addColumn));
// Write the whole log line:
PrintWriter out = getOutput(level, context);
out.println(buf.toString());
// Print the stack trace, if any:
printException(error, out);
out.flush();
}
/**
* <p>Format and print the given exception inside the given writer.</p>
*
* <p>This function does nothing if the given error is NULL.</p>
*
* <p>The full stack trace is printed ONLY for unknown exceptions.</p>
*
* <p>The printed text has the following format for known exceptions:</p>
* <pre>
* Caused by a {ExceptionClassName} {ExceptionOrigin}
* {ExceptionMessage}
* </pre>
*
* <p>The printed text has the following format for unknown exceptions:</p>
* <pre>
* Caused by a {ExceptionFullStackTrace}
* </pre>
*
* @param error The exception to print.
* @param out The output in which the exception must be written.
*
* @see #getExceptionOrigin(Throwable)
*
* @since 4.1
*/
protected void printException(final Throwable error, final PrintWriter out){
if (error != null){
if (error instanceof UWSException){
if (error.getCause() != null)
printException(error.getCause(), out);
else{
out.println("Caused by a " + error.getClass().getName() + " " + getExceptionOrigin(error));
if (error.getMessage() != null)
out.println("\t" + error.getMessage());
}
}else{
out.print("Caused by a ");
error.printStackTrace(out);
}
}
}
/**
* <p>Format and return the origin of the given error.
* "Origin" means here: "where the error has been thrown from?" (from which class? method? file? line?).</p>
*
* <p>This function does nothing if the given error is NULL or if the origin information is missing.</p>
*
* <p>The returned text has the following format:</p>
* <pre>
* at {OriginClass}.{OriginMethod}({OriginFile}:{OriginLine})
* </pre>
*
* <p>{OriginFile} and {OriginLine} are written only if provided.</p>
*
* @param error Error whose the origin should be returned.
*
* @return A string which contains formatted information about the origin of the given error.
*
* @since 4.1
*/
protected String getExceptionOrigin(final Throwable error){
if (error != null && error.getStackTrace() != null && error.getStackTrace().length > 0){
StackTraceElement src = error.getStackTrace()[0];
return "at " + src.getClassName() + "." + src.getMethodName() + ((src.getFileName() != null) ? "(" + src.getFileName() + ((src.getLineNumber() >= 0) ? ":" + src.getLineNumber() : "") + ")" : "");
}else
return "";
}
@Override
public void debug(String msg){
log(LogLevel.DEBUG, null, msg, null);
}
@Override
public void debug(Throwable t){
log(LogLevel.DEBUG, null, null, t);
}
@Override
public void debug(String msg, Throwable t){
log(LogLevel.DEBUG, null, msg, t);
}
@Override
public void info(String msg){
log(LogLevel.INFO, null, msg, null);
}
@Override
public void warning(String msg){
log(LogLevel.WARNING, null, msg, null);
}
@Override
public void error(String msg){
log(LogLevel.ERROR, null, msg, null);
}
@Override
public void error(Throwable t){
log(LogLevel.ERROR, null, null, t);
}
@Override
public void error(String msg, Throwable t){
log(LogLevel.ERROR, null, msg, t);
}
/* ************* */
/* HTTP ACTIVITY */
/* ************* */
/**
* <p>A message/error logged with this function will have the following format:</p>
* <pre><TIMESTAMP> <LEVEL> HTTP REQUEST_RECEIVED <REQUEST_ID> <MESSAGE> <HTTP_METHOD> in <CONTENT_TYPE> at <URL> from <IP_ADDR> using <USER_AGENT> with parameters (<PARAM1>=<VAL1>&...)</pre>
*
* @see uws.service.log.UWSLog#logHttp(uws.service.log.UWSLog.LogLevel, javax.servlet.http.HttpServletRequest, java.lang.String, java.lang.String, java.lang.Throwable)
*/
@Override
public void logHttp(LogLevel level, final HttpServletRequest request, final String requestId, final String message, final Throwable error){
// IF A REQUEST IS PROVIDED, write its details after the message in a new column:
if (request != null){
// If the type is missing:
if (level == null)
level = (error != null) ? LogLevel.ERROR : LogLevel.INFO;
// Log or not?
if (!canLog(level))
return;
StringBuffer str = new StringBuffer();
// Write the request type, content type and the URL:
str.append(request.getMethod());
str.append(" as ");
if (request.getContentType() != null){
if (request.getContentType().indexOf(';') > 0)
str.append(request.getContentType().substring(0, request.getContentType().indexOf(';')));
else
str.append(request.getContentType());
}
str.append(" at ").append(request.getRequestURL());
// Write the IP address:
str.append(" from ").append(request.getRemoteAddr());
// Write the user agent:
str.append(" using ").append(request.getHeader("User-Agent") == null ? "" : request.getHeader("User-Agent"));
// Write the posted parameters:
str.append(" with parameters (");
Map<String,String> params = UWSToolBox.getParamsMap(request);
int i = -1;
for(Entry<String,String> p : params.entrySet()){
if (++i > 0)
str.append('&');
str.append(p.getKey()).append('=').append((p.getValue() != null) ? p.getValue() : "");
}
str.append(')');
// Send the log message to the log file:
log(level, "HTTP", "REQUEST_RECEIVED", requestId, (message != null ? message : str.toString()), (message != null ? str.toString() : null), error);
}
// OTHERWISE, just write the given message:
else
log(level, "HTTP", "REQUEST_RECEIVED", requestId, message, null, error);
}
/**
* <p>A message/error logged with this function will have the following format:</p>
* <pre><TIMESTAMP> <LEVEL> HTTP RESPONSE_SENT <REQUEST_ID> <MESSAGE> HTTP-<STATUS_CODE> to the user <USER> as <CONTENT_TYPE></pre>
* <p>,where <USER> may be either "(id:<USER_ID>;pseudo:<USER_PSEUDO>)" or "ANONYMOUS".</p>
*
* @see uws.service.log.UWSLog#logHttp(uws.service.log.UWSLog.LogLevel, javax.servlet.http.HttpServletResponse, java.lang.String, uws.job.user.JobOwner, java.lang.String, java.lang.Throwable)
*/
@Override
public void logHttp(LogLevel level, HttpServletResponse response, String requestId, JobOwner user, String message, Throwable error){
if (response != null){
// If the type is missing:
if (level == null)
level = (error != null) ? LogLevel.ERROR : LogLevel.INFO;
// Log or not?
if (!canLog(level))
return;
StringBuffer str = new StringBuffer();
// Write the response status code:
str.append("HTTP-").append(response.getStatus());
// Write the user to whom the response is sent:
str.append(" to the user ");
if (user != null){
str.append("(id:").append(user.getID());
if (user.getPseudo() != null)
str.append(";pseudo:").append(user.getPseudo());
str.append(')');
}else
str.append("ANONYMOUS");
// Write the response's MIME type:
if (response.getContentType() != null)
str.append(" as ").append(response.getContentType());
// Send the log message to the log file:
log(level, "HTTP", "RESPONSE_SENT", requestId, message, str.toString(), error);
}
// OTHERWISE, just write the given message:
else
log(level, "HTTP", "RESPONSE_SENT", requestId, message, null, error);
}
/* ************ */
/* UWS ACTIVITY */
/* ************ */
@Override
public void logUWS(LogLevel level, Object obj, String event, String message, Throwable error){
// If the type is missing:
if (level == null)
level = (error != null) ? LogLevel.ERROR : LogLevel.INFO;
// Log or not?
if (!canLog(level))
return;
// CASE "BACKUPED": Append to the message the backup report:
String report = null;
if (event != null && event.equalsIgnoreCase("BACKUPED") && obj != null && obj.getClass().getName().equals("[I")){
int[] backupReport = (int[])obj;
if (backupReport.length == 2)
report = "(" + backupReport[0] + "/" + backupReport[1] + " jobs backuped for this user)";
else
report = "(" + backupReport[0] + "/" + backupReport[1] + " jobs backuped ; " + backupReport[2] + "/" + backupReport[3] + " users backuped)";
}else if (event != null && event.equalsIgnoreCase("RESTORED") && obj != null && obj.getClass().getName().equals("[I")){
int[] restoreReport = (int[])obj;
report = "(" + restoreReport[0] + "/" + restoreReport[1] + " jobs restored ; " + restoreReport[2] + "/" + restoreReport[3] + " users restored)";
}
// Log the message
log(level, "UWS", event, null, message, report, error);
}
/* ************ */
/* JOB ACTIVITY */
/* ************ */
@Override
public void logJob(LogLevel level, UWSJob job, String event, String message, Throwable error){
log(level, "JOB", event, (job == null) ? null : job.getJobId(), message, null, error);
}
/* ********************** */
/* THREAD STATUS MESSAGES */
/* ********************** */
@Override
public void logThread(LogLevel level, Thread thread, String event, String message, Throwable error){
if (thread != null){
// If the type is missing:
if (level == null)
level = (error != null) ? LogLevel.ERROR : LogLevel.INFO;
// Log or not?
if (!canLog(level))
return;
StringBuffer str = new StringBuffer();
// Write the thread name and ID:
str.append(thread.getName()).append(" (thread ID: ").append(thread.getId()).append(")");
// Write the thread state:
str.append(" is ").append(thread.getState());
// Write its thread group name:
str.append(" in the group " + thread.getThreadGroup().getName());
// Write the number of active threads:
str.append(" where ").append(thread.getThreadGroup().activeCount()).append(" threads are active");
log(level, "THREAD", event, thread.getName(), message, str.toString(), error);
}else
log(level, "THREAD", event, null, message, null, error);
}
}