/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt
*
* 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.tasks;
import com.google.common.collect.Lists;
import hudson.Extension;
import hudson.model.Job;
import hudson.model.Run;
import jenkins.model.BuildDiscarder;
import jenkins.model.BuildDiscarderDescriptor;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
/**
* Default implementation of {@link BuildDiscarder}.
*
* For historical reason, this is called LogRotator, but it does not rotate logs :-)
*
* Since 1.350 it has also the option to keep the build, but delete its recorded artifacts.
*
* @author Kohsuke Kawaguchi
*/
public class LogRotator extends BuildDiscarder {
/**
* If not -1, history is only kept up to this days.
*/
private final int daysToKeep;
/**
* If not -1, only this number of build logs are kept.
*/
private final int numToKeep;
/**
* If not -1 nor null, artifacts are only kept up to this days.
* Null handling is necessary to remain data compatible with old versions.
* @since 1.350
*/
private final Integer artifactDaysToKeep;
/**
* If not -1 nor null, only this number of builds have their artifacts kept.
* Null handling is necessary to remain data compatible with old versions.
* @since 1.350
*/
private final Integer artifactNumToKeep;
@DataBoundConstructor
public LogRotator (String daysToKeepStr, String numToKeepStr, String artifactDaysToKeepStr, String artifactNumToKeepStr) {
this (parse(daysToKeepStr),parse(numToKeepStr),
parse(artifactDaysToKeepStr),parse(artifactNumToKeepStr));
}
public static int parse(String p) {
if(p==null) return -1;
try {
return Integer.parseInt(p);
} catch (NumberFormatException e) {
return -1;
}
}
/**
* @deprecated since 1.350.
* Use {@link #LogRotator(int, int, int, int)}
*/
@Deprecated
public LogRotator(int daysToKeep, int numToKeep) {
this(daysToKeep, numToKeep, -1, -1);
}
public LogRotator(int daysToKeep, int numToKeep, int artifactDaysToKeep, int artifactNumToKeep) {
this.daysToKeep = daysToKeep;
this.numToKeep = numToKeep;
this.artifactDaysToKeep = artifactDaysToKeep;
this.artifactNumToKeep = artifactNumToKeep;
}
@SuppressWarnings("rawtypes")
public void perform(Job<?,?> job) throws IOException, InterruptedException {
LOGGER.log(FINE, "Running the log rotation for {0} with numToKeep={1} daysToKeep={2} artifactNumToKeep={3} artifactDaysToKeep={4}", new Object[] {job, numToKeep, daysToKeep, artifactNumToKeep, artifactDaysToKeep});
// always keep the last successful and the last stable builds
Run lsb = job.getLastSuccessfulBuild();
Run lstb = job.getLastStableBuild();
if(numToKeep!=-1) {
// Note that RunList.size is deprecated, and indeed here we are loading all the builds of the job.
// However we would need to load the first numToKeep anyway, just to skip over them;
// and we would need to load the rest anyway, to delete them.
// (Using RunMap.headMap would not suffice, since we do not know if some recent builds have been deleted for other reasons,
// so simply subtracting numToKeep from the currently last build number might cause us to delete too many.)
List<? extends Run<?,?>> builds = job.getBuilds();
for (Run r : copy(builds.subList(Math.min(builds.size(), numToKeep), builds.size()))) {
if (shouldKeepRun(r, lsb, lstb)) {
continue;
}
LOGGER.log(FINE, "{0} is to be removed", r);
r.delete();
}
}
if(daysToKeep!=-1) {
Calendar cal = new GregorianCalendar();
cal.add(Calendar.DAY_OF_YEAR,-daysToKeep);
Run r = job.getFirstBuild();
while (r != null) {
if (tooNew(r, cal)) {
break;
}
if (!shouldKeepRun(r, lsb, lstb)) {
LOGGER.log(FINE, "{0} is to be removed", r);
r.delete();
}
r = r.getNextBuild();
}
}
if(artifactNumToKeep!=null && artifactNumToKeep!=-1) {
List<? extends Run<?,?>> builds = job.getBuilds();
for (Run r : copy(builds.subList(Math.min(builds.size(), artifactNumToKeep), builds.size()))) {
if (shouldKeepRun(r, lsb, lstb)) {
continue;
}
LOGGER.log(FINE, "{0} is to be purged of artifacts", r);
r.deleteArtifacts();
}
}
if(artifactDaysToKeep!=null && artifactDaysToKeep!=-1) {
Calendar cal = new GregorianCalendar();
cal.add(Calendar.DAY_OF_YEAR,-artifactDaysToKeep);
Run r = job.getFirstBuild();
while (r != null) {
if (tooNew(r, cal)) {
break;
}
if (!shouldKeepRun(r, lsb, lstb)) {
LOGGER.log(FINE, "{0} is to be purged of artifacts", r);
r.deleteArtifacts();
}
r = r.getNextBuild();
}
}
}
private boolean shouldKeepRun(Run r, Run lsb, Run lstb) {
if (r.isKeepLog()) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s marked as a keeper", r);
return true;
}
if (r == lsb) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s the last successful build", r);
return true;
}
if (r == lstb) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s the last stable build", r);
return true;
}
if (r.isBuilding()) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s still building", r);
return true;
}
return false;
}
private boolean tooNew(Run r, Calendar cal) {
if (!r.getTimestamp().before(cal)) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s still new", r);
return true;
} else {
return false;
}
}
/**
* Creates a copy since we'll be deleting some entries from them.
*/
private <R> Collection<R> copy(Iterable<R> src) {
return Lists.newArrayList(src);
}
public int getDaysToKeep() {
return daysToKeep;
}
public int getNumToKeep() {
return numToKeep;
}
public int getArtifactDaysToKeep() {
return unbox(artifactDaysToKeep);
}
public int getArtifactNumToKeep() {
return unbox(artifactNumToKeep);
}
public String getDaysToKeepStr() {
return toString(daysToKeep);
}
public String getNumToKeepStr() {
return toString(numToKeep);
}
public String getArtifactDaysToKeepStr() {
return toString(artifactDaysToKeep);
}
public String getArtifactNumToKeepStr() {
return toString(artifactNumToKeep);
}
private int unbox(Integer i) {
return i==null ? -1: i;
}
private String toString(Integer i) {
if (i==null || i==-1) return "";
return String.valueOf(i);
}
@Extension @Symbol("logRotator")
public static final class LRDescriptor extends BuildDiscarderDescriptor {
public String getDisplayName() {
return "Log Rotation";
}
}
private static final Logger LOGGER = Logger.getLogger(LogRotator.class.getName());
}