/*
* Copyright 2011 Future Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.araqne.logstorage.engine;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.araqne.confdb.ConfigService;
import org.araqne.cron.PeriodicJob;
import org.araqne.logstorage.DiskLackAction;
import org.araqne.logstorage.DiskLackCallback;
import org.araqne.logstorage.DiskSpaceType;
import org.araqne.logstorage.LogRetentionPolicy;
import org.araqne.logstorage.LogStorage;
import org.araqne.logstorage.LogStorageMonitor;
import org.araqne.logstorage.LogStorageStatus;
import org.araqne.logstorage.LogTableRegistry;
import org.araqne.logstorage.TableLockedException;
import org.araqne.storage.api.FilePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@PeriodicJob("* * * * *")
@Component(name = "logstorage-monitor")
@Provides
public class LogStorageMonitorEngine implements LogStorageMonitor {
private static final String DEFAULT_MIN_FREE_SPACE_TYPE = DiskSpaceType.Percentage.toString();
private static final int DEFAULT_MIN_FREE_SPACE_VALUE = 5;
private static final String DEFAULT_DISK_LACK_ACTION = DiskLackAction.StopLogging.toString();
private final Logger logger = LoggerFactory.getLogger(LogStorageMonitorEngine.class.getName());
@Requires
private LogTableRegistry tableRegistry;
@Requires
private LogStorage storage;
@Requires
private ConfigService conf;
// last check time, check retention policy and purge files for every hour
private long lastPurgeCheck;
private DiskSpaceType minFreeSpaceType;
private int minFreeSpaceValue;
private DiskLackAction diskLackAction;
private Set<DiskLackCallback> diskLackCallbacks = new HashSet<DiskLackCallback>();
private boolean stopByLowDisk;
public LogStorageMonitorEngine() {
reload();
}
private void reload() {
minFreeSpaceType = DiskSpaceType.valueOf(getStringParameter(Constants.MinFreeDiskSpaceType, DEFAULT_MIN_FREE_SPACE_TYPE));
minFreeSpaceValue = getIntParameter(Constants.MinFreeDiskSpaceValue, DEFAULT_MIN_FREE_SPACE_VALUE);
diskLackAction = DiskLackAction.valueOf(getStringParameter(Constants.DiskLackAction, DEFAULT_DISK_LACK_ACTION));
}
@Override
public int getMinFreeSpaceValue() {
return minFreeSpaceValue;
}
@Override
public DiskSpaceType getMinFreeSpaceType() {
return minFreeSpaceType;
}
@Override
public void setMinFreeSpace(int value, DiskSpaceType type) {
if (type == DiskSpaceType.Percentage) {
if (value <= 0 || value >= 100)
throw new IllegalArgumentException("invalid value");
} else if (type == DiskSpaceType.Megabyte) {
if (value <= 0)
throw new IllegalArgumentException("invalid value");
} else if (type == null)
throw new IllegalArgumentException("type cannot be null");
this.minFreeSpaceType = type;
this.minFreeSpaceValue = value;
ConfigUtil.set(conf, Constants.MinFreeDiskSpaceType, type.toString());
ConfigUtil.set(conf, Constants.MinFreeDiskSpaceValue, Integer.toString(value));
}
@Override
public DiskLackAction getDiskLackAction() {
return diskLackAction;
}
@Override
public void setDiskLackAction(DiskLackAction action) {
if (action == null)
throw new IllegalArgumentException("action cannot be null");
this.diskLackAction = action;
ConfigUtil.set(conf, Constants.DiskLackAction, action.toString());
}
@Override
public void registerDiskLackCallback(DiskLackCallback callback) {
diskLackCallbacks.add(callback);
}
@Override
public void unregisterDiskLackCallback(DiskLackCallback callback) {
diskLackCallbacks.remove(callback);
}
@Override
public void forceRetentionCheck() {
checkRetentions(true);
}
private boolean isDiskLack(FilePath dir) {
if (!dir.exists())
return false;
long usable = dir.getUsableSpace();
long total = dir.getTotalSpace();
logger.trace("araqne logstorage: check {} {} free space of partition [{}], current [{}] total [{}]", new Object[] {
minFreeSpaceValue, minFreeSpaceType.toString().toLowerCase(), dir.getAbsolutePath(), usable, total });
if (total == 0) {
logger.error("araqne logstorage: low disk, dir [{}] usable [{}] total [{}]", new Object[] {
dir.getAbsoluteFilePath(), usable, total });
return true;
}
String unit = (minFreeSpaceType == DiskSpaceType.Percentage ? "%" : "MB");
if (minFreeSpaceType == DiskSpaceType.Percentage) {
int percent = (int) (usable * 100 / total);
if (percent < minFreeSpaceValue) {
logger.error("araqne logstorage: low disk, dir [{}] usable [{}] total [{}] percent [{}] threshold [{} {}]",
new Object[] { dir.getAbsoluteFilePath(), usable, total, percent, minFreeSpaceValue, unit });
return true;
}
} else if (minFreeSpaceType == DiskSpaceType.Megabyte) {
int mega = (int) (usable / 1048576);
if (mega < minFreeSpaceValue) {
logger.error("araqne logstorage: low disk, dir [{}] usable [{}] total [{}] percent [{}] threshold [{} {}]",
new Object[] { dir.getAbsoluteFilePath(), usable, total, mega, minFreeSpaceValue, unit });
return true;
}
}
return false;
}
@Override
public void run() {
try {
runOnce();
} catch (Exception e) {
logger.error("araqne logstorage: storage monitor error", e);
}
}
private void runOnce() {
reload();
checkRetentions(false);
checkDiskLack();
}
private void checkRetentions(boolean force) {
long now = System.currentTimeMillis();
if (!force && now - lastPurgeCheck < 3600 * 1000)
return;
for (String tableName : tableRegistry.getTableNames()) {
checkAndPurgeFiles(tableName);
}
lastPurgeCheck = now;
}
private void checkAndPurgeFiles(String tableName) {
LogRetentionPolicy p = storage.getRetentionPolicy(tableName);
if (p == null || p.getRetentionDays() == 0) {
logger.debug("araqne logstorage: no retention policy for table [{}]", tableName);
return;
}
// purge tables
Date logBaseline = storage.getPurgeBaseline(tableName);
if (logBaseline != null) {
try {
storage.purge(tableName, null, logBaseline);
} catch (TableLockedException e) {
logger.debug("purge skipped: table is locked by {}", e.getMessage());
}
}
}
private void checkDiskLack() {
// categorize by partition path
Map<FilePath, List<String>> partitionTables = new HashMap<FilePath, List<String>>();
for (String tableName : tableRegistry.getTableNames()) {
FilePath tableDir = storage.getTableDirectory(tableName);
if (tableDir == null)
continue;
FilePath dir = tableDir.getAbsoluteFilePath().getParentFilePath();
List<String> tables = partitionTables.get(dir);
if (tables == null) {
tables = new ArrayList<String>();
partitionTables.put(dir, tables);
}
tables.add(tableName);
}
boolean lowDisk = false;
for (FilePath dir : partitionTables.keySet()) {
lowDisk |= checkDiskPartitions(dir, partitionTables.get(dir));
}
if (lowDisk) {
for (DiskLackCallback callback : diskLackCallbacks) {
try {
callback.callback();
} catch (Throwable t) {
logger.warn("araqne logstorage: disk lack callback should not throw any exception", t);
}
}
} else {
// open log storage if low disk problem is resolved
if (stopByLowDisk)
startStorage();
}
}
private boolean checkDiskPartitions(FilePath partitionPath, List<String> tableNames) {
if (isDiskLack(partitionPath)) {
logger.error("araqne logstorage: not enough disk space, current minimum free space config [{}] {}",
minFreeSpaceValue, (minFreeSpaceType == DiskSpaceType.Percentage ? "%" : "MB"));
if (diskLackAction == DiskLackAction.StopLogging) {
if (storage.getStatus() == LogStorageStatus.Open) {
stopStorage(partitionPath);
}
} else if (diskLackAction == DiskLackAction.RemoveOldLog) {
List<LogFile> files = new ArrayList<LogFile>();
for (String tableName : tableNames) {
for (Date date : storage.getLogDates(tableName))
files.add(new LogFile(tableName, date));
}
Collections.sort(files, new LogFileComparator());
int index = 0;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
do {
if (index >= files.size()) {
if (storage.getStatus() == LogStorageStatus.Open) {
stopStorage(partitionPath);
}
break;
}
LogFile lf = files.get(index++);
logger.info("araqne logstorage: removing old log, table [{}], date [{}]", lf.tableName, sdf.format(lf.date));
storage.purge(lf.tableName, lf.date);
} while (isDiskLack(partitionPath));
}
return true;
}
return false;
}
private void startStorage() {
logger.info("araqne logstorage: low disk problem is resolved, restart logstorage");
storage.start();
stopByLowDisk = false;
}
private void stopStorage(FilePath partitionPath) {
logger.error("araqne logstorage: [{}] not enough space, stop logging", partitionPath.getAbsolutePath());
storage.stop();
stopByLowDisk = true;
}
private String getStringParameter(Constants key, String defaultValue) {
String value = ConfigUtil.get(conf, key);
if (value != null)
return value;
return defaultValue;
}
private int getIntParameter(Constants key, int defaultValue) {
String value = ConfigUtil.get(conf, key);
if (value != null)
return Integer.valueOf(value);
return defaultValue;
}
private class LogFile {
private String tableName;
private Date date;
private LogFile(String tableName, Date date) {
this.tableName = tableName;
this.date = date;
}
}
private class LogFileComparator implements Comparator<LogFile> {
@Override
public int compare(LogFile o1, LogFile o2) {
return o1.date.compareTo(o2.date);
}
}
}