/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geogig.geoserver.config;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.geogig.geoserver.config.LogStoreInitializer.copySampleInitSript;
import static org.geogig.geoserver.config.LogStoreInitializer.createDefaultConfig;
import static org.geogig.geoserver.config.LogStoreInitializer.newDataSource;
import static org.geogig.geoserver.config.LogStoreInitializer.runScript;
import static org.geogig.geoserver.config.LogStoreInitializer.saveConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import javax.annotation.Nullable;
import javax.sql.DataSource;
import org.geogig.geoserver.config.LogEvent.Severity;
import org.geoserver.config.GeoServer;
import org.geoserver.config.impl.GeoServerLifecycleHandler;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.ResourceStore;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.db.DBAppender;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.core.db.DataSourceConnectionSource;
import ch.qos.logback.core.util.StatusPrinter;
public class LogStore implements GeoServerLifecycleHandler, InitializingBean {
private static final char MSG_FIELD_SEPARATOR = '|';
static final String PROP_ENABLED = "enabled";
static final String PROP_MAX_CONNECTIONS = "maxConnections";
static final String PROP_PASSWORD = "password";
static final String PROP_USER = "user";
static final String PROP_DRIVER_CLASS = "driverClass";
static final String PROP_URL = "url";
static final String PROP_SCRIPT = "initScript";
static final String PROP_RUN_SCRIPT = "runInitScript";
static final String CONFIG_DIR_NAME = "geogig/config/security";
static final String CONFIG_FILE_NAME = "logstore.properties";
private Logger LOGBACKLOGGER;
private File configFile;
private DataSource dataSource;
private ResourceStore resourceStore;
private volatile boolean enabled;
public LogStore(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
/**
* Called when geoserver is being shutdown
*/
@Override
public void onDispose() {
destroy();
}
/**
* Called as part of {@link GeoServer#reset()} execution to clear up all of the caches inside
* GeoServer forcing reloading of all information besides the configuration itself
*/
@Override
public void onReset() {
//
}
/**
* Called as {@link GeoServer#reload()} begins its work. A subsequent call to
* {@link #onReload()} is guaranteed
*/
@Override
public void beforeReload() {
destroy();
}
/**
* Called as part of the {@link GeoServer#reload()} process to clear up all of the caches as
* well as the configuration information
*/
@Override
public void onReload() {
init();
}
void destroy() {
enabled = false;
if (dataSource != null) {
DataSource dataSource = this.dataSource;
this.dataSource = null;
this.LOGBACKLOGGER = null;
this.configFile = null;
LogStoreInitializer.dispose(dataSource);
}
}
private void init() {
try {
this.configFile = findOrCreateConfigFile();
} catch (IOException e) {
throw Throwables.propagate(e);
}
Properties properties = new Properties();
try (InputStream in = new FileInputStream(configFile)) {
properties.load(in);
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("properties file does not exist: " + configFile);
} catch (IOException e) {
throw new RuntimeException("Error loading properties file " + configFile, e);
}
boolean enabled = Boolean.valueOf(properties.getProperty(PROP_ENABLED));
if (enabled) {
dataSource = newDataSource(properties, configFile);
boolean runScript = Boolean.valueOf(properties.getProperty(PROP_RUN_SCRIPT));
if (runScript) {
String scriptProp = properties.getProperty(PROP_SCRIPT);
URL script = resolveScript(scriptProp, configFile);
runScript(dataSource, script);
// all good, lets disable the script for the next runs
properties.setProperty(PROP_RUN_SCRIPT, "false");
saveConfig(properties, configFile);
}
LOGBACKLOGGER = createLogger(dataSource);
}
this.enabled = enabled;
}
private URL resolveScript(String scriptProp, File configFile) {
File scriptFile = new File(scriptProp);
if (scriptFile.isAbsolute()) {
checkArgument(scriptFile.exists(), "Script file %s does not exist", scriptFile);
}
// find it relative to config file
scriptFile = new File(configFile.getParentFile(), scriptProp);
checkArgument(scriptFile.exists(), "Script file %s does not exist",
scriptFile.getAbsolutePath());
try {
return scriptFile.toURI().toURL();
} catch (MalformedURLException e) {
throw Throwables.propagate(e);
}
}
public void debug(@Nullable String repoUrl, @Nullable CharSequence message) {
if (enabled && message != null) {
String msg = buildMessage(repoUrl, message);
LOGBACKLOGGER.debug(msg);
}
}
public void info(@Nullable String repoUrl, @Nullable CharSequence message) {
if (enabled && message != null) {
String msg = buildMessage(repoUrl, message);
LOGBACKLOGGER.info(msg);
}
}
public void error(@Nullable String repoUrl, @Nullable CharSequence message, Throwable exception) {
if (enabled && message != null) {
String msg = buildMessage(repoUrl, message);
LOGBACKLOGGER.error(msg, exception);
}
}
public int getFullSize() {
try (Connection c = dataSource.getConnection()) {
String sql = "SELECT count(*) from logging_event";
try (ResultSet rs = c.createStatement().executeQuery(sql)) {
rs.next();
int fullSize = rs.getInt(1);
return fullSize;
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
/**
* @param offset unlike JDBC offset, this offset starts at zero, not at one
*/
public List<LogEvent> getLogEntries(final int offset, final int limit) {
return getLogEntries(offset, limit, (LogEvent.Severity[]) null);
}
/**
* @param offset unlike JDBC offset, this offset starts at zero, not at one
* @param limit max number of entries to retrieve
* @param severity filter logs by severity
*/
public List<LogEvent> getLogEntries(final int offset, final int limit,
final @Nullable LogEvent.Severity... severity) {
checkState(enabled, "LogStore has not been initialized");
checkArgument(offset >= 0);
checkArgument(limit >= 0);
StringBuilder sql = new StringBuilder(
"SELECT event_id, timestmp, level_string, formatted_message FROM logging_event ");
if (severity != null) {
sql.append("WHERE level_string IN(");
for (Iterator<Severity> it = Arrays.asList(severity).iterator(); it.hasNext();) {
Severity s = it.next();
sql.append('\'').append(s.toString()).append('\'');
if (it.hasNext()) {
sql.append(", ");
}
}
sql.append(")");
}
sql.append(" ORDER BY event_id DESC LIMIT ").append(limit);
sql.append(" OFFSET ").append(offset);
try (Connection c = dataSource.getConnection()) {
try (ResultSet rs = c.createStatement().executeQuery(sql.toString())) {
List<LogEvent> events = parseEventList(rs, c);
return events;
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Nullable
public String getStackTrace(long eventId) {
try (Connection c = dataSource.getConnection()) {
return getStackTrace(eventId, c);
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
private String getStackTrace(long eventId, Connection c) throws SQLException {
String sql = "SELECT trace_line FROM logging_event_exception WHERE event_id = ? ORDER BY i";
StringBuilder sb = new StringBuilder();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, eventId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String traceLine = rs.getString(1);
sb.append(traceLine).append('\n');
}
}
}
return sb.length() == 0 ? null : sb.toString();
}
private List<LogEvent> parseEventList(ResultSet rs, Connection c) throws SQLException {
List<LogEvent> list = new LinkedList<LogEvent>();
long eventId;
long timestamp;
String levelString;
String formattedMessage;
List<String> msgParts;
String repoUrl;
String user;
String message;
while (rs.next()) {
eventId = rs.getLong(1);
timestamp = rs.getLong(2);
levelString = rs.getString(3);
formattedMessage = rs.getString(4);
msgParts = Splitter.on(MSG_FIELD_SEPARATOR).splitToList(formattedMessage);
Preconditions.checkState(msgParts.size() == 3, "Unknown message format: %s",
formattedMessage);
repoUrl = msgParts.get(0);
user = msgParts.get(1);
message = msgParts.get(2);
Severity severity = LogEvent.Severity.valueOf(levelString);
LogEvent e = new LogEvent(eventId, timestamp, severity, repoUrl, user, message);
list.add(e);
}
return list;
}
/**
* @return a string composed of {@code <repoUrl>|<user>|<message>}
*/
private String buildMessage(@Nullable String repoUrl, @Nullable CharSequence message) {
return new StringBuilder(url(repoUrl)).append(MSG_FIELD_SEPARATOR).append(user())
.append(MSG_FIELD_SEPARATOR).append(message).toString();
}
private String url(String repoUrl) {
return Strings.isNullOrEmpty(repoUrl) ? "" : repoUrl;
}
private static String user() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String name = auth == null ? null : auth.getName();
if (name == null) {
name = "anonymous";
}
return name;
}
private File findOrCreateConfigFile() throws IOException {
Resource dirResource = resourceStore.get(CONFIG_DIR_NAME);
File dir = dirResource.dir();
File configFile = new File(dir, CONFIG_FILE_NAME);
copySampleInitSript(dir, "mysql.sql");
copySampleInitSript(dir, "postgresql.sql");
copySampleInitSript(dir, "postgresql.properties");
copySampleInitSript(dir, "sqlite.sql");
copySampleInitSript(dir, "hsqldb.sql");
copySampleInitSript(dir, "hsqldb.properties");
if (!configFile.exists()) {
createDefaultConfig(configFile);
}
return configFile;
}
private static Logger createLogger(DataSource dataSource) {
LoggerContext lc = new LoggerContext();
PatternLayoutEncoder ple = new PatternLayoutEncoder();
ple.setPattern("%d{\"yyyy-MM-dd'T'HH:mm:ss.SSS\"},%level,%msg%n");
ple.setContext(lc);
ple.start();
DataSourceConnectionSource connectionSource = new DataSourceConnectionSource();
connectionSource.setContext(lc);
connectionSource.setDataSource(dataSource);
connectionSource.start();
DBAppender dbAppender = new DBAppender();
dbAppender.setContext(lc);
dbAppender.setConnectionSource(connectionSource);
dbAppender.start();
// ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<ILoggingEvent>();
// consoleAppender.setContext(lc);
// consoleAppender.setTarget("System.err");
// consoleAppender.setEncoder(ple);
// consoleAppender.start();
lc.start();
Logger logger = lc.getLogger(LogStore.class);
// logger.addAppender(consoleAppender);
logger.addAppender(dbAppender);
logger.setLevel(Level.DEBUG);
StatusPrinter.print(lc);
return logger;
}
}