/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.logging;
import com.thoughtworks.xstream.XStream;
import hudson.BulkChange;
import hudson.Extension;
import hudson.FilePath;
import hudson.Util;
import hudson.XmlFile;
import hudson.model.*;
import hudson.util.HttpResponses;
import jenkins.model.Jenkins;
import hudson.model.listeners.SaveableListener;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.slaves.ComputerListener;
import hudson.util.CopyOnWriteList;
import hudson.util.RingBufferLogHandler;
import hudson.util.XStream2;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.*;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.text.Collator;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* 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 jenkins.model.Jenkins#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;
public final CopyOnWriteList<Target> targets = new CopyOnWriteList<Target>();
@Restricted(NoExternalUse.class)
Target[] orderedTargets() {
// will contain targets ordered by reverse name length (place specific targets at the beginning)
Target[] ts = targets.toArray(new Target[]{});
Arrays.sort(ts, new Comparator<Target>() {
public int compare(Target left, Target right) {
return right.getName().length() - left.getName().length();
}
});
return ts;
}
@Restricted(NoExternalUse.class)
public AutoCompletionCandidates doAutoCompleteLoggerName(@QueryParameter String value) {
AutoCompletionCandidates candidates = new AutoCompletionCandidates();
Enumeration<String> loggerNames = LogManager.getLogManager().getLoggerNames();
while (loggerNames.hasMoreElements()) {
String loggerName = loggerNames.nextElement();
if (loggerName.toLowerCase(Locale.ENGLISH).contains(value.toLowerCase(Locale.ENGLISH))) {
candidates.add(loggerName);
}
}
return candidates;
}
@Restricted(NoExternalUse.class)
transient /*almost final*/ RingBufferLogHandler handler = new RingBufferLogHandler() {
@Override
public void publish(LogRecord record) {
for (Target t : orderedTargets()) {
Boolean match = t.matches(record);
if (match == null) {
// domain does not match, so continue looking
continue;
}
if (match.booleanValue()) {
// most specific logger matches, so publish
super.publish(record);
}
// most specific logger does not match, so don't publish
// allows reducing log level for more specific loggers
return;
}
}
};
/**
* 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;
private transient /* almost final*/ Logger logger;
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));
}
public Level getLevel() {
return Level.parse(String.valueOf(level));
}
public String getName() {
return name;
}
@Deprecated
public boolean includes(LogRecord r) {
if(r.getLevel().intValue() < level)
return false; // below the threshold
if (name.length() == 0) {
return true; // like root logger, includes everything
}
String logName = r.getLoggerName();
if(logName==null || !logName.startsWith(name))
return false; // not within this logger
String rest = logName.substring(name.length());
return rest.startsWith(".") || rest.length()==0;
}
public Boolean matches(LogRecord r) {
boolean levelSufficient = r.getLevel().intValue() >= level;
if (name.length() == 0) {
return Boolean.valueOf(levelSufficient); // include if level matches
}
String logName = r.getLoggerName();
if(logName==null || !logName.startsWith(name))
return null; // not in the domain of this logger
String rest = logName.substring(name.length());
if (rest.startsWith(".") || rest.length()==0) {
return Boolean.valueOf(levelSufficient); // include if level matches
}
return null;
}
public Logger getLogger() {
if (logger == null) {
logger = Logger.getLogger(name);
}
return logger;
}
/**
* 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());
new SetLevel(name, getLevel()).broadcast();
}
public void disable() {
getLogger().setLevel(null);
new SetLevel(name, null).broadcast();
}
}
private static final class SetLevel extends MasterToSlaveCallable<Void,Error> {
/** known loggers (kept per agent), to avoid GC */
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private static final Set<Logger> loggers = new HashSet<Logger>();
private final String name;
private final Level level;
SetLevel(String name, Level level) {
this.name = name;
this.level = level;
}
@Override public Void call() throws Error {
Logger logger = Logger.getLogger(name);
loggers.add(logger);
logger.setLevel(level);
return null;
}
void broadcast() {
for (Computer c : Jenkins.getInstance().getComputers()) {
if (c.getName().length() > 0) { // i.e. not master
VirtualChannel ch = c.getChannel();
if (ch != null) {
try {
ch.call(this);
} catch (Exception x) {
Logger.getLogger(LogRecorder.class.getName()).log(Level.WARNING, "could not set up logging on " + c, x);
}
}
}
}
}
}
@Extension @Restricted(NoExternalUse.class) public static final class ComputerLogInitializer extends ComputerListener {
@Override public void preOnline(Computer c, Channel channel, FilePath root, TaskListener listener) throws IOException, InterruptedException {
for (LogRecorder recorder : Jenkins.getInstance().getLog().logRecorders.values()) {
for (Target t : recorder.targets) {
channel.call(new SetLevel(t.name, t.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 Util.rawEncode(name);
}
public String getName() {
return name;
}
public LogRecorderManager getParent() {
return Jenkins.getInstance().getLog();
}
/**
* Accepts submission from the configuration page.
*/
@RequirePOST
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)) {
Jenkins.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);
}
@RequirePOST
public HttpResponse doClear() throws IOException {
handler.clear();
return HttpResponses.redirectToDot();
}
/**
* 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.
*/
@RequirePOST
public synchronized void doDoDelete(StaplerResponse rsp) throws IOException, ServletException {
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.disable();
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,getLogRecords());
}
/**
* The file we save our configuration.
*/
private XmlFile getConfigFile() {
return new XmlFile(XSTREAM, new File(LogRecorderManager.configDir(), name + ".xml"));
}
/**
* Gets a view of the log records.
*/
public List<LogRecord> getLogRecords() {
return handler.getView();
}
/**
* Gets a view of log records per agent matching this recorder.
* @return a map (sorted by display name) from computer to (nonempty) list of log records
* @since 1.519
*/
public Map<Computer,List<LogRecord>> getSlaveLogRecords() {
Map<Computer,List<LogRecord>> result = new TreeMap<Computer,List<LogRecord>>(new Comparator<Computer>() {
final Collator COLL = Collator.getInstance();
public int compare(Computer c1, Computer c2) {
return COLL.compare(c1.getDisplayName(), c2.getDisplayName());
}
});
for (Computer c : Jenkins.getInstance().getComputers()) {
if (c.getName().length() == 0) {
continue; // master
}
List<LogRecord> recs = new ArrayList<LogRecord>();
try {
for (LogRecord rec : c.getLogRecords()) {
for (Target t : targets) {
if (t.includes(rec)) {
recs.add(rec);
break;
}
}
}
} catch (IOException x) {
continue;
} catch (InterruptedException x) {
continue;
}
if (!recs.isEmpty()) {
result.put(c, recs);
}
}
return result;
}
/**
* 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.ALL, Level.FINEST, Level.FINER, Level.FINE, Level.CONFIG, Level.INFO, Level.WARNING, Level.SEVERE);
}