/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* 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
*
* Contributors:
*
* Kohsuke Kawaguchi
*
*
*******************************************************************************/
package hudson.logging;
import com.thoughtworks.xstream.XStream;
import hudson.BulkChange;
import hudson.Util;
import hudson.XmlFile;
import hudson.model.AbstractModelObject;
import hudson.model.Hudson;
import hudson.model.Saveable;
import hudson.model.listeners.SaveableListener;
import hudson.util.CopyOnWriteList;
import hudson.util.RingBufferLogHandler;
import hudson.util.XStream2;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
/**
* Records a selected set of logs so that the system administrator can diagnose
* a specific aspect of the system.
*
* TODO: still a work in progress.
*
* <h3>Access Control</h3> {@link LogRecorder} is only visible for
* administrators, and this access control happens at {@link Hudson#getLog()},
* the sole entry point for binding {@link LogRecorder} to URL.
*
* @author Kohsuke Kawaguchi
* @see LogRecorderManager
*/
public class LogRecorder extends AbstractModelObject implements Saveable {
private volatile String name;
//TODO: review and check whether we can do it private
public final CopyOnWriteList<Target> targets = new CopyOnWriteList<Target>();
private transient /*almost final*/ RingBufferLogHandler handler = new RingBufferLogHandler() {
@Override
public void publish(LogRecord record) {
for (Target t : targets) {
if (t.includes(record)) {
super.publish(record);
return;
}
}
}
};
public CopyOnWriteList<Target> getTargets() {
return targets;
}
/**
* Logger that this recorder monitors, and its log level. Just a pair of
* (logger name,level) with convenience methods.
*/
public static final class Target {
public final String name;
private final int level;
public Target(String name, Level level) {
this(name, level.intValue());
}
public Target(String name, int level) {
this.name = name;
this.level = level;
}
@DataBoundConstructor
public Target(String name, String level) {
this(name, Level.parse(level.toUpperCase(Locale.ENGLISH)));
}
public Level getLevel() {
return Level.parse(String.valueOf(level));
}
public boolean includes(LogRecord r) {
if (r.getLevel().intValue() < level) {
return false; // below the threshold
}
String logName = r.getLoggerName();
if (logName == null || !logName.startsWith(name)) {
return false; // not within this logger
}
String rest = r.getLoggerName().substring(name.length());
return rest.startsWith(".") || rest.length() == 0;
}
public Logger getLogger() {
return Logger.getLogger(name);
}
/**
* Makes sure that the logger passes through messages at the correct
* level to us.
*/
public void enable() {
Logger l = getLogger();
if (!l.isLoggable(getLevel())) {
l.setLevel(getLevel());
}
}
}
public LogRecorder(String name) {
this.name = name;
// register it only once when constructed, and when this object dies
// WeakLogHandler will remove it
new WeakLogHandler(handler, Logger.getLogger(""));
}
public String getDisplayName() {
return name;
}
public String getSearchUrl() {
return name;
}
public String getName() {
return name;
}
public LogRecorderManager getParent() {
return Hudson.getInstance().getLog();
}
/**
* Accepts submission from the configuration page.
*/
public synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
JSONObject src = req.getSubmittedForm();
String newName = src.getString("name"), redirect = ".";
XmlFile oldFile = null;
if (!name.equals(newName)) {
Hudson.checkGoodName(newName);
oldFile = getConfigFile();
// rename
getParent().logRecorders.remove(name);
this.name = newName;
getParent().logRecorders.put(name, this);
redirect = "../" + Util.rawEncode(newName) + '/';
}
List<Target> newTargets = req.bindJSONToList(Target.class, src.get("targets"));
for (Target t : newTargets) {
t.enable();
}
targets.replaceBy(newTargets);
save();
if (oldFile != null) {
oldFile.delete();
}
rsp.sendRedirect2(redirect);
}
/**
* Loads the settings from a file.
*/
public synchronized void load() throws IOException {
getConfigFile().unmarshal(this);
for (Target t : targets) {
t.enable();
}
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
}
/**
* Deletes this recorder, then go back to the parent.
*/
public synchronized void doDoDelete(StaplerResponse rsp) throws IOException, ServletException {
requirePOST();
getConfigFile().delete();
getParent().logRecorders.remove(name);
// Disable logging for all our targets,
// then reenable all other loggers in case any also log the same targets
for (Target t : targets) {
t.getLogger().setLevel(null);
}
for (LogRecorder log : getParent().logRecorders.values()) {
for (Target t : log.targets) {
t.enable();
}
}
rsp.sendRedirect2("..");
}
/**
* RSS feed for log entries.
*/
public void doRss(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
LogRecorderManager.doRss(req, rsp, getDisplayName(), getLogRecords());
}
/**
* The file we save our configuration.
*/
private XmlFile getConfigFile() {
return new XmlFile(XSTREAM, new File(Hudson.getInstance().getRootDir(), "log/" + name + ".xml"));
}
/**
* Gets a view of the log records.
*/
public List<LogRecord> getLogRecords() {
return handler.getView();
}
/**
* Thread-safe reusable {@link XStream}.
*/
public static final XStream XSTREAM = new XStream2();
static {
XSTREAM.alias("log", LogRecorder.class);
XSTREAM.alias("target", Target.class);
}
/**
* Log levels that can be configured for {@link Target}.
*/
public static List<Level> LEVELS =
Arrays.asList(Level.SEVERE, Level.WARNING, Level.INFO, Level.CONFIG,
Level.FINE, Level.FINER, Level.FINEST, Level.ALL);
}