/* * The MIT License * * Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi, Martin Eigenbrodt, * Anton Kozak, Nikita Levyankov * * 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 hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Job; import hudson.model.Run; import hudson.scm.SCM; import java.io.IOException; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; import java.util.logging.Logger; import org.kohsuke.stapler.DataBoundConstructor; import static java.util.logging.Level.FINE; import static java.util.logging.Level.FINER; /** * Deletes old log files. * * TODO: is there any other task that follows the same pattern? * try to generalize this just like {@link SCM} or {@link BuildStep}. * * @author Kohsuke Kawaguchi */ public class LogRotator implements Describable<LogRotator> { private static final Logger LOGGER = Logger.getLogger(LogRotator.class.getName()); /** * 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 logrotate_days, String logrotate_nums, String logrotate_artifact_days, String logrotate_artifact_nums) { this (parse(logrotate_days),parse(logrotate_nums), parse(logrotate_artifact_days),parse(logrotate_artifact_nums)); } 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)} */ 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; } public void perform(Job<?, ?> job) throws IOException, InterruptedException { LOGGER.log(FINE, "Running the log rotation for " + job.getFullDisplayName()); // keep the last successful build regardless of the status Run lsb = job.getLastSuccessfulBuild(); Run lstb = job.getLastStableBuild(); List<? extends Run<?, ?>> builds = job.getBuilds(); Calendar cal = null; //Delete builds if (-1 != numToKeep || -1 != daysToKeep) { if (-1 != daysToKeep) { cal = new GregorianCalendar(); cal.add(Calendar.DAY_OF_YEAR, -daysToKeep); } if (-1 != numToKeep) { builds = builds.subList(Math.min(builds.size(), numToKeep), builds.size()); } //Delete builds based on configured values. See http://issues.hudson-ci.org/browse/HUDSON-3650 deleteBuilds(builds, lsb, lstb, cal); } cal = null; builds = job.getBuilds(); //Delete build artifacts if (-1 != artifactNumToKeep || -1 != artifactDaysToKeep) { if (-1 != artifactDaysToKeep) { cal = new GregorianCalendar(); cal.add(Calendar.DAY_OF_YEAR, -artifactDaysToKeep); } if (-1 != artifactNumToKeep) { builds = builds.subList(Math.min(builds.size(), artifactNumToKeep), builds.size()); } //Delete build artifacts based on configured values. See http://issues.hudson-ci.org/browse/HUDSON-3650 deleteBuildArtifacts(builds, lsb, lstb, cal); } } /** * Performs builds deletion * * @param builds list of builds * @param lastSuccessBuild last success build * @param lastStableBuild last stable build * @param cal calendar if configured * @throws IOException if configured */ private void deleteBuilds(List<? extends Run<?, ?>> builds, Run lastSuccessBuild, Run lastStableBuild, Calendar cal) throws IOException { for (Run currentBuild : builds) { if (allowDeleteBuild(lastSuccessBuild, lastStableBuild, currentBuild, cal)) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is to be removed"); currentBuild.delete(); } } } /** * Checks whether current build could be deleted. * If current build equals to last Success Build or last Stable Build or currentBuild is configured to keep logs or * currentBuild timestamp is before configured calendar value - return false, otherwise return true. * * @param lastSuccessBuild {@link Run} * @param lastStableBuild {@link Run} * @param currentBuild {@link Run} * @param cal {@link Calendar} * @return true - if deletion is allowed, false - otherwise. */ private boolean allowDeleteBuild(Run lastSuccessBuild, Run lastStableBuild, Run currentBuild, Calendar cal) { if (currentBuild.isKeepLog()) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not GC-ed because it's marked as a keeper"); return false; } if (currentBuild == lastSuccessBuild) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not GC-ed because it's the last successful build"); return false; } if (currentBuild == lastStableBuild) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not GC-ed because it's the last stable build"); return false; } if (null != cal && !currentBuild.getTimestamp().before(cal)) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not GC-ed because it's still new"); return false; } return true; } /** * Performs build artifacts deletion * * @param builds list of builds * @param lastSuccessBuild last success build * @param lastStableBuild last stable build * @param cal calendar if configured * @throws IOException if configured */ private void deleteBuildArtifacts(List<? extends Run<?, ?>> builds, Run lastSuccessBuild, Run lastStableBuild, Calendar cal) throws IOException { for (Run currentBuild : builds) { if (allowDeleteArtifact(lastSuccessBuild, lastStableBuild, currentBuild, cal)) { currentBuild.deleteArtifacts(); } } } /** * Checks whether artifacts from build could be deleted. * If current build equals to last Success Build or last Stable Build or currentBuild is configured to keep logs or * currentBuild timestamp is before configured calendar value - return false, otherwise return true. * * @param lastSuccessBuild {@link Run} * @param lastStableBuild {@link Run} * @param currentBuild {@link Run} * @param cal {@link Calendar} * @return true - if deletion is allowed, false - otherwise. */ private boolean allowDeleteArtifact(Run lastSuccessBuild, Run lastStableBuild, Run currentBuild, Calendar cal) { if (currentBuild.isKeepLog()) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not purged of artifacts because it's marked as a keeper"); return false; } if (currentBuild == lastSuccessBuild) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not purged of artifacts because it's the last successful build"); return false; } if (currentBuild == lastStableBuild) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not purged of artifacts because it's the last stable build"); return false; } if (null != cal && !currentBuild.getTimestamp().before(cal)) { LOGGER.log(FINER, currentBuild.getFullDisplayName() + " is not purged of artifacts because it's still new"); return false; } return true; } 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); } public LRDescriptor getDescriptor() { return DESCRIPTOR; } public static final LRDescriptor DESCRIPTOR = new LRDescriptor(); public static final class LRDescriptor extends Descriptor<LogRotator> { public String getDisplayName() { return "Log Rotation"; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LogRotator that = (LogRotator) o; if (daysToKeep != that.daysToKeep) { return false; } if (numToKeep != that.numToKeep) { return false; } if (artifactDaysToKeep != null ? !artifactDaysToKeep.equals(that.artifactDaysToKeep) : that.artifactDaysToKeep != null) { return false; } if (artifactNumToKeep != null ? !artifactNumToKeep.equals(that.artifactNumToKeep) : that.artifactNumToKeep != null) { return false; } return true; } @Override public int hashCode() { int result = daysToKeep; result = 31 * result + numToKeep; result = 31 * result + (artifactDaysToKeep != null ? artifactDaysToKeep.hashCode() : 0); result = 31 * result + (artifactNumToKeep != null ? artifactNumToKeep.hashCode() : 0); return result; } }