001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *      http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.oozie.util;
019    
020    import java.io.File;
021    import java.util.ArrayList;
022    import java.util.Calendar;
023    import java.util.Collections;
024    import java.util.concurrent.Semaphore;
025    import java.util.regex.Matcher;
026    import org.apache.log4j.Appender;
027    import org.apache.log4j.rolling.RollingPolicyBase;
028    import org.apache.log4j.rolling.RolloverDescription;
029    import org.apache.log4j.rolling.TimeBasedRollingPolicy;
030    import org.apache.log4j.rolling.TriggeringPolicy;
031    import org.apache.log4j.spi.LoggingEvent;
032    import org.apache.oozie.service.Services;
033    import org.apache.oozie.service.XLogService;
034    
035    /**
036     * Has the same behavior as the TimeBasedRollingPolicy.  Additionally, it will delete older logs (MaxHistory determines how many
037     * older logs are retained).
038     */
039    public class OozieRollingPolicy extends RollingPolicyBase implements TriggeringPolicy {
040    
041        /**
042         * Unfortunately, TimeBasedRollingPolicy is declared final, so we can't subclass it; instead, we have to wrap it
043         */
044        private TimeBasedRollingPolicy tbrp;
045        
046        private Semaphore deleteSem;
047        
048        private Thread deleteThread;
049        
050        private int maxHistory = 720;       // (720 hours / 24 hours per day = 30 days) as default
051        
052        public int getMaxHistory() {
053            return maxHistory;
054        }
055    
056        public void setMaxHistory(int maxHistory) {
057            this.maxHistory = maxHistory;
058        }
059        
060        public OozieRollingPolicy() {
061            deleteSem = new Semaphore(1);
062            deleteThread = new Thread();
063            tbrp = new TimeBasedRollingPolicy();
064        }
065        
066        @Override
067        public void activateOptions() {
068            super.activateOptions();
069            tbrp.setFileNamePattern(getFileNamePattern());
070            tbrp.activateOptions();
071        }
072        
073        @Override
074        public RolloverDescription initialize(String file, boolean append) throws SecurityException {
075            return tbrp.initialize(file, append);
076        }
077        
078        @Override
079        public RolloverDescription rollover(final String activeFile) throws SecurityException {
080            return tbrp.rollover(activeFile);
081        }
082        
083        @Override
084        public boolean isTriggeringEvent(final Appender appender, final LoggingEvent event, final String filename, 
085        final long fileLength) {
086            if (maxHistory >= 0) {  // -1 = disable
087                // Only delete old logs if we're not already deleting logs and another thread hasn't already started setting up to delete
088                // the old logs
089                if (deleteSem.tryAcquire()) {
090                    if (!deleteThread.isAlive()) {
091                        // Do the actual deleting in a new thread in case its slow so we don't bottleneck anything else
092                        deleteThread = new Thread() {
093                            @Override
094                            public void run() {
095                                deleteOldFiles();
096                            }
097                        };
098                        deleteThread.start();
099                    }
100                    deleteSem.release();
101                }
102            }
103            return tbrp.isTriggeringEvent(appender, event, filename, fileLength);
104        }
105        
106        private void deleteOldFiles() {
107            ArrayList<FileInfo> fileList = new ArrayList<FileInfo>();
108            XLogService xls = getXLogService();
109            if (xls != null) {      // We need this to get the paths
110                String oozieLogPath = xls.getOozieLogPath();
111                String logFile = xls.getOozieLogName();
112                if (oozieLogPath != null && logFile != null) {
113                    String[] children = new File(oozieLogPath).list();
114                    if (children != null) {
115                        for (String child : children) {
116                            if (child.startsWith(logFile) && !child.equals(logFile)) {
117                                File childFile = new File(new File(oozieLogPath).getAbsolutePath(), child);
118                                if (child.endsWith(".gz")) {
119                                    long gzFileCreationTime = getGZFileCreationTime(child);
120                                    if (gzFileCreationTime != -1) {
121                                        fileList.add(new FileInfo(childFile.getAbsolutePath(), gzFileCreationTime));
122                                    }
123                                } else{ 
124                                    long modTime = childFile.lastModified();
125                                    fileList.add(new FileInfo(childFile.getAbsolutePath(), modTime));
126                                }
127                            }
128                        }
129                    }
130                }
131            }
132            
133            if (fileList.size() > maxHistory) {
134                Collections.sort(fileList);
135                
136                for (int i = maxHistory; i < fileList.size(); i++) {
137                    new File(fileList.get(i).getFileName()).delete();
138                }
139            }
140        }
141        
142        private long getGZFileCreationTime(String fileName) {
143            // Default return value of -1 to exclude the file
144            long returnVal = -1;
145            Matcher m = XLogStreamer.gzTimePattern.matcher(fileName);
146            if (m.matches() && m.groupCount() == 4) {
147                int year = Integer.parseInt(m.group(1));
148                int month = Integer.parseInt(m.group(2));
149                int day = Integer.parseInt(m.group(3));
150                int hour = Integer.parseInt(m.group(4));
151                int minute = 0;
152                Calendar calendarEntry = Calendar.getInstance();
153                calendarEntry.set(year, month - 1, day, hour, minute); // give month-1(Say, 7 for August)
154                long logFileStartTime = calendarEntry.getTimeInMillis();
155                returnVal = logFileStartTime;
156            }
157            return returnVal;
158        }
159        
160        class FileInfo implements Comparable<FileInfo> {
161            String fileName;
162            long modTime;
163    
164            public FileInfo(String fileName, long modTime) {
165                this.fileName = fileName;
166                this.modTime = modTime;
167            }
168    
169            public String getFileName() {
170                return fileName;
171            }
172    
173            public long getModTime() {
174                return modTime;
175            }
176    
177            public int compareTo(FileInfo fileInfo) {
178                // Note: the order is the reverse of XLogStreamer.FileInfo
179                long diff = fileInfo.modTime - this.modTime;
180                if (diff > 0) {
181                    return 1;
182                }
183                else if (diff < 0) {
184                    return -1;
185                }
186                else {
187                    return 0;
188                }
189            }
190        }
191        
192        // Needed for TestOozieRollingPolicy tests to be able to override getOozieLogPath() and getOozieLogName()
193        // by overriding getXLogService()
194        XLogService getXLogService() {
195            if (Services.get() != null) {
196                return Services.get().get(XLogService.class);
197            }
198            return null;
199        }
200    }