/*
* 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.logdb.query.engine;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Validate;
import org.araqne.api.SystemProperty;
import org.araqne.confdb.ConfigService;
import org.araqne.cron.TickService;
import org.araqne.log.api.LogParserFactoryRegistry;
import org.araqne.log.api.LogParserRegistry;
import org.araqne.log.api.LoggerRegistry;
import org.araqne.logdb.AbstractQueryCommandParser;
import org.araqne.logdb.AccountService;
import org.araqne.logdb.DefaultQuery;
import org.araqne.logdb.FunctionRegistry;
import org.araqne.logdb.LookupHandlerRegistry;
import org.araqne.logdb.MetadataService;
import org.araqne.logdb.ProcedureRegistry;
import org.araqne.logdb.Query;
import org.araqne.logdb.QueryCommand;
import org.araqne.logdb.QueryCommandParser;
import org.araqne.logdb.QueryContext;
import org.araqne.logdb.QueryEventListener;
import org.araqne.logdb.QueryParseException;
import org.araqne.logdb.QueryParserService;
import org.araqne.logdb.QueryPlanner;
import org.araqne.logdb.QueryResultFactory;
import org.araqne.logdb.QueryScriptRegistry;
import org.araqne.logdb.QueryService;
import org.araqne.logdb.QueryStatus;
import org.araqne.logdb.QueryStatusCallback;
import org.araqne.logdb.QueryStopReason;
import org.araqne.logdb.RunMode;
import org.araqne.logdb.SavedResultManager;
import org.araqne.logdb.Session;
import org.araqne.logdb.SessionEventListener;
import org.araqne.logdb.query.parser.BoxPlotParser;
import org.araqne.logdb.query.parser.BypassParser;
import org.araqne.logdb.query.parser.CheckTableParser;
import org.araqne.logdb.query.parser.ConfdbParser;
import org.araqne.logdb.query.parser.CsvFileParser;
import org.araqne.logdb.query.parser.DropParser;
import org.araqne.logdb.query.parser.EvalParser;
import org.araqne.logdb.query.parser.EvalcParser;
import org.araqne.logdb.query.parser.ExecParser;
import org.araqne.logdb.query.parser.ExplodeParser;
import org.araqne.logdb.query.parser.FieldsParser;
import org.araqne.logdb.query.parser.ImportParser;
import org.araqne.logdb.query.parser.InsertParser;
import org.araqne.logdb.query.parser.JoinParser;
import org.araqne.logdb.query.parser.JsonFileParser;
import org.araqne.logdb.query.parser.JsonParser;
import org.araqne.logdb.query.parser.LimitParser;
import org.araqne.logdb.query.parser.LoadParser;
import org.araqne.logdb.query.parser.LoggerParser;
import org.araqne.logdb.query.parser.LookupParser;
import org.araqne.logdb.query.parser.MemLookupParser;
import org.araqne.logdb.query.parser.MvParser;
import org.araqne.logdb.query.parser.OutputCsvParser;
import org.araqne.logdb.query.parser.OutputJsonParser;
import org.araqne.logdb.query.parser.OutputTxtParser;
import org.araqne.logdb.query.parser.ParseCsvParser;
import org.araqne.logdb.query.parser.ParseJsonParser;
import org.araqne.logdb.query.parser.ParseKvParser;
import org.araqne.logdb.query.parser.ParseMapParser;
import org.araqne.logdb.query.parser.ParseParser;
import org.araqne.logdb.query.parser.ParseXmlParser;
import org.araqne.logdb.query.parser.PrevParser;
import org.araqne.logdb.query.parser.ProcParser;
import org.araqne.logdb.query.parser.PurgeParser;
import org.araqne.logdb.query.parser.RateLimitParser;
import org.araqne.logdb.query.parser.RenameParser;
import org.araqne.logdb.query.parser.RepeatParser;
import org.araqne.logdb.query.parser.ResultParser;
import org.araqne.logdb.query.parser.RexParser;
import org.araqne.logdb.query.parser.SearchParser;
import org.araqne.logdb.query.parser.SetParser;
import org.araqne.logdb.query.parser.SignatureParser;
import org.araqne.logdb.query.parser.SortParser;
import org.araqne.logdb.query.parser.StatsParser;
import org.araqne.logdb.query.parser.SystemCommandParser;
import org.araqne.logdb.query.parser.TableParser;
import org.araqne.logdb.query.parser.TextFileParser;
import org.araqne.logdb.query.parser.TimechartParser;
import org.araqne.logdb.query.parser.ToJsonParser;
import org.araqne.logdb.query.parser.UnionParser;
import org.araqne.logdb.query.parser.ZipFileParser;
import org.araqne.logstorage.Log;
import org.araqne.logstorage.LogFileServiceRegistry;
import org.araqne.logstorage.LogStorage;
import org.araqne.logstorage.LogTableRegistry;
import org.araqne.logstorage.StorageConfig;
import org.araqne.logstorage.TableNotFoundException;
import org.araqne.logstorage.TableSchema;
import org.araqne.storage.crypto.LogCryptoService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(name = "logdb-query")
@Provides(specifications = { QueryService.class })
public class QueryServiceImpl implements QueryService, SessionEventListener {
private static final String QUERY_LOG_TABLE = "araqne_query_logs";
private final Logger logger = LoggerFactory.getLogger(QueryServiceImpl.class);
@Requires
private ConfigService conf;
@Requires
private TickService tickService;
@Requires
private AccountService accountService;
@Requires
private LogTableRegistry tableRegistry;
@Requires
private LookupHandlerRegistry lookupRegistry;
@Requires
private QueryScriptRegistry scriptRegistry;
@Requires
private LogParserFactoryRegistry parserFactoryRegistry;
@Requires
private LogParserRegistry parserRegistry;
@Requires
private QueryParserService queryParserService;
@Requires
private LogStorage storage;
@Requires
private LogFileServiceRegistry fileServiceRegistry;
@Requires
private MetadataService metadataService;
@Requires
private SavedResultManager savedResultManager;
@Requires
private LoggerRegistry loggerRegistry;
@Requires
private QueryResultFactory resultFactory;
@Requires
private FunctionRegistry functionRegistry;
@Requires
private ProcedureRegistry procedureRegistry;
@Requires
private LogCryptoService cryptoService;
private ConcurrentMap<Integer, Query> queries;
private CopyOnWriteArraySet<QueryEventListener> callbacks;
private List<QueryCommandParser> queryParsers;
private List<QueryPlanner> planners;
private boolean allowQueryPurge = false;
private boolean useBom = false;
public QueryServiceImpl() {
this.queries = new ConcurrentHashMap<Integer, Query>();
this.callbacks = new CopyOnWriteArraySet<QueryEventListener>();
this.planners = new CopyOnWriteArrayList<QueryPlanner>();
// ensure directory
File dir = new File(System.getProperty("araqne.data.dir"), "araqne-logdb/query");
dir.mkdirs();
allowQueryPurge = SystemProperty.isEnabled("araqne.logdb.allowpurge");
allowQueryPurge = SystemProperty.isEnabled("araqne.logdb.purge");
useBom = SystemProperty.isEnabled("araqne.logdb.utf8bom");
prepareQueryParsers();
}
private void prepareQueryParsers() {
@SuppressWarnings("unchecked")
List<Class<? extends AbstractQueryCommandParser>> parserClazzes = Arrays.asList(DropParser.class, EvalParser.class,
EvalcParser.class, SearchParser.class, StatsParser.class, FieldsParser.class, SortParser.class,
TimechartParser.class, RenameParser.class, RexParser.class, JsonParser.class, SignatureParser.class,
LimitParser.class, SetParser.class, BoxPlotParser.class, ParseKvParser.class, ExplodeParser.class,
ParseJsonParser.class, ExecParser.class, ParseMapParser.class, ParseXmlParser.class, CsvFileParser.class,
PrevParser.class);
List<QueryCommandParser> parsers = new ArrayList<QueryCommandParser>();
for (Class<? extends AbstractQueryCommandParser> clazz : parserClazzes) {
try {
parsers.add(clazz.newInstance());
} catch (Exception e) {
logger.error("araqne logdb: failed to add syntax: " + clazz.getSimpleName(), e);
}
}
// add table and lookup (need some constructor injection)
parsers.add(new TableParser(accountService, storage, tableRegistry, parserFactoryRegistry, parserRegistry));
parsers.add(new LookupParser(lookupRegistry));
// parsers.add(new ScriptParser(bc, scriptRegistry));
parsers.add(new TextFileParser(parserFactoryRegistry));
parsers.add(new ZipFileParser(parserFactoryRegistry));
parsers.add(new JsonFileParser(parserFactoryRegistry));
parsers.add(new ToJsonParser());
parsers.add(new OutputCsvParser(tickService, useBom));
parsers.add(new OutputJsonParser(tickService));
parsers.add(new OutputTxtParser(tickService));
parsers.add(new SystemCommandParser("logdb", metadataService)); // deprecated
parsers.add(new SystemCommandParser("system", metadataService));
parsers.add(new CheckTableParser(tableRegistry, storage, fileServiceRegistry, cryptoService));
parsers.add(new JoinParser(queryParserService, resultFactory));
parsers.add(new UnionParser(queryParserService));
parsers.add(new ImportParser(tableRegistry, storage));
parsers.add(new ParseParser(parserRegistry));
parsers.add(new LoadParser(savedResultManager));
parsers.add(new LoggerParser(loggerRegistry));
parsers.add(new MvParser());
parsers.add(new ConfdbParser(conf));
parsers.add(new InsertParser(tableRegistry, storage));
parsers.add(new ParseCsvParser());
parsers.add(new ProcParser(accountService, queryParserService, procedureRegistry, this));
parsers.add(new RateLimitParser(tickService));
parsers.add(new MemLookupParser(lookupRegistry));
parsers.add(new BypassParser());
parsers.add(new RepeatParser());
parsers.add(new ResultParser(this));
if (allowQueryPurge)
parsers.add(new PurgeParser(storage, tableRegistry));
this.queryParsers = parsers;
}
@Validate
public void start() {
// NOTE: do not call ensureTable directly, it can cause iPOJO hang.
new Thread(new Runnable() {
@Override
public void run() {
storage.ensureTable(new TableSchema(QUERY_LOG_TABLE, new StorageConfig("v2")));
}
}, "Araqne Query Log Table Creator").start();
for (QueryCommandParser p : queryParsers)
queryParserService.addCommandParser(p);
accountService.addListener(this);
// delete all temporary query files
File queryResultDir = new File(System.getProperty("araqne.data.dir"), "araqne-logdb/query/");
File[] resultFiles = queryResultDir.listFiles();
if (resultFiles == null)
return;
for (File f : resultFiles) {
String name = f.getName();
if (name.startsWith("result") && (name.endsWith(".idx") || name.endsWith(".dat")))
f.delete();
}
// delete all temporary sort files
String dataDir = System.getProperty("araqne.sort.dir", System.getProperty("araqne.data.dir"));
File sortDir = new File(dataDir, "araqne-logdb/sort");
File[] runFiles = sortDir.listFiles();
if (runFiles == null)
return;
for (File f : runFiles) {
String name = f.getName();
if (name.startsWith("run") && (name.endsWith(".idx") || name.endsWith(".dat")))
f.delete();
}
}
@Invalidate
public void stop() {
for (int id : queries.keySet()) {
Query query = queries.get(id);
if (query != null) {
logger.info("araqne logdb: cancel query [{}:{}] due to service down", id, query.getQueryString());
removeQuery(id);
}
}
if (accountService != null) {
accountService.removeListener(this);
}
if (queryParserService != null) {
for (QueryCommandParser p : queryParsers)
queryParserService.removeCommandParser(p);
}
}
@Override
public Query createQuery(Session session, String queryString) {
return createQuery(new QueryContext(session), queryString);
}
@Override
public Query createQuery(QueryContext context, String queryString) {
Session session = context.getSession();
if (logger.isDebugEnabled())
logger.debug("araqne logdb: try to create query [{}] from session [{}:{}]", new Object[] { queryString,
session == null ? null : session.getGuid(), session == null ? null : session.getLoginName() });
List<QueryCommand> commands = null;
try {
commands = queryParserService.parseCommands(context, queryString);
for (QueryPlanner planner : planners)
commands = planner.plan(context, commands);
} catch (QueryParseException e) {
// write log(query execution failed)
HashMap<String, Object> m = new HashMap<String, Object>();
m.put("state", "parse_failure");
if (session != null) {
String source = context.getSource();
m.put("source", (source != null) ? source : "webconsole");
m.put("login_name", session.getLoginName());
}
m.put("query_string", queryString);
m.put("error_code", e.getType());
m.put("error_msg", e.getMessage());
Date now = new Date();
writeLog(now, m);
throw e;
}
Query query = new DefaultQuery(context, queryString, commands, resultFactory);
queries.put(query.getId(), query);
query.getCallbacks().getStatusCallbacks().add(new EofReceiver());
invokeCallbacks(query, QueryStatus.CREATED);
return query;
}
@Override
public void startQuery(int id) {
startQuery(null, id);
}
@Override
public void startQuery(Session session, int id) {
Query query = getQuery(id);
if (query == null)
throw new IllegalArgumentException("invalid log query id: " + id);
if (session != null && !query.isAccessible(session))
throw new IllegalArgumentException("invalid log query id: " + id);
new Thread(query, "Query " + id).start();
HashMap<String, Object> m = new HashMap<String, Object>();
m.put("state", "started");
if (session != null) {
String source = query.getContext().getSource();
m.put("source", (source != null) ? source : "webconsole");
m.put("login_name", session.getLoginName());
m.put("remote_ip", session.getProperty("remote_ip"));
} else {
m.put("source", null);
m.put("login_name", null);
}
m.put("start_at", new Date());
m.put("query_string", query.getQueryString());
m.put("query_id", query.getId());
try {
m.put("constants", query.getContext().getConstants());
} catch (Throwable t) {
m.put("constants", null);
}
writeLog(new Date(), m);
invokeCallbacks(query, QueryStatus.STARTED);
}
@Override
public void removeQuery(int id) {
removeQuery(null, id);
}
@Override
public void removeQuery(Session session, int id) {
if (logger.isDebugEnabled()) {
if (session == null) {
logger.debug("araqne logdb: try to remove query [{}]", id);
} else {
logger.debug("araqne logdb: try to remove query [{}] from session [{}:{}]",
new Object[] { id, session.getGuid(), session.getLoginName() });
}
}
Query query = queries.remove(id);
if (query == null) {
logger.debug("araqne logdb: query [{}] not found, remove failed", id);
return;
}
if (session != null && !query.isAccessible(session)) {
Session querySession = query.getContext().getSession();
logger.warn("araqne logdb: security violation, [{}] access to query of login [{}] session [{}]",
new Object[] { session.getLoginName(), querySession.getLoginName(), querySession.getGuid() });
return;
}
try {
query.getCallbacks().getStatusCallbacks().clear();
query.getResult().getResultCallbacks().clear();
if (query.isStarted() && !query.isFinished())
query.cancel(QueryStopReason.UserRequest);
} catch (Throwable t) {
logger.error("araqne logdb: cannot cancel query " + query, t);
}
try {
query.purge();
} catch (Throwable t) {
logger.error("araqne logdb: cannot close file buffer list for query " + query.getId(), t);
}
invokeCallbacks(query, QueryStatus.REMOVED);
}
@Override
public Collection<Query> getQueries() {
return queries.values();
}
@Override
public Collection<Query> getQueries(Session session) {
List<Query> l = new ArrayList<Query>();
for (Query q : queries.values())
if (q.isAccessible(session))
l.add(q);
return l;
}
@Override
public Query getQuery(int id) {
return queries.get(id);
}
@Override
public Query getQuery(Session session, int id) {
Query q = queries.get(id);
if (q == null)
return null;
if (!q.isAccessible(session))
return null;
return q;
}
@Override
public void addListener(QueryEventListener listener) {
callbacks.add(listener);
}
@Override
public void removeListener(QueryEventListener listener) {
callbacks.remove(listener);
}
@Override
public List<QueryPlanner> getPlanners() {
return new ArrayList<QueryPlanner>(planners);
}
@Override
public QueryPlanner getPlanner(String name) {
for (QueryPlanner planner : planners)
if (planner.getName().equals(name))
return planner;
return null;
}
@Override
public void addPlanner(QueryPlanner planner) {
planners.add(planner);
}
@Override
public void removePlanner(QueryPlanner planner) {
planners.remove(planner);
}
private void invokeCallbacks(Query query, QueryStatus status) {
logger.debug("araqne logdb: invoking callback to notify query [{}], status [{}]", query.getId(), status);
for (QueryEventListener callback : callbacks) {
try {
callback.onQueryStatusChange(query, status);
} catch (Exception e) {
logger.warn("araqne logdb: query event listener should not throw any exception", e);
}
}
}
/**
* @since 0.17.0
*/
@Override
public void onLogin(Session session) {
}
/**
* @since 0.17.0
*/
@Override
public void onLogout(Session session) {
for (Query q : queries.values()) {
if (q.getContext() == null || q.getContext().getSession() == null)
continue;
Session s = q.getContext().getSession();
if (q.getRunMode() == RunMode.FOREGROUND && s.equals(session)) {
logger.trace("araqne logdb: removing foreground query [{}:{}] by session [{}] logout",
new Object[] { q.getId(), q.getQueryString(), session.getLoginName() });
removeQuery(q.getId());
}
}
}
private void serializeQuery(Query query, Date now, HashMap<String, Object> m) {
Session session = query.getContext().getSession();
m.put("query_id", query.getId());
m.put("query_string", query.getQueryString());
try {
m.put("rows", query.getResultCount());
} catch (IOException e) {
m.put("rows", 0);
}
m.put("start_at", new Date(query.getStartTime()));
m.put("eof_at", now);
if (session != null)
m.put("login_name", session.getLoginName());
else
m.put("login_name", null);
m.put("cancelled", query.isCancelled());
try {
m.put("constants", query.getContext().getConstants());
} catch (Throwable t) {
m.put("constants", null);
}
if (query.isFinished()) {
m.put("state", "stopped");
} else {
m.put("state", "running");
}
if (query.getStopReason() != null)
m.put("stop_reason", query.getStopReason().toString());
if (query.isStarted() && query.getFinishTime() > 0)
m.put("duration", (query.getFinishTime() - query.getStartTime()) / 1000.0);
else
m.put("duration", 0);
if (session != null) {
String source = query.getContext().getSource();
m.put("source", (source != null) ? source : "webconsole");
m.put("remote_ip", session.getProperty("remote_ip"));
}
}
private void writeLog(Date now, HashMap<String, Object> m) {
try {
storage.write(new Log(QUERY_LOG_TABLE, now, m));
} catch (InterruptedException e) {
logger.warn("writing query log is interrupted: {}", m);
} catch (TableNotFoundException e) {
storage.ensureTable(new TableSchema(QUERY_LOG_TABLE, new StorageConfig("v2")));
try {
storage.write(new Log(QUERY_LOG_TABLE, now, m));
} catch (InterruptedException e1) {
}
}
}
private class EofReceiver implements QueryStatusCallback {
@Override
public void onChange(Query query) {
if (!query.isFinished())
return;
// prevent duplicated logging
query.getCallbacks().getStatusCallbacks().remove(this);
Date now = new Date();
HashMap<String, Object> m = new HashMap<String, Object>();
serializeQuery(query, now, m);
writeLog(now, m);
invokeCallbacks(query, QueryStatus.EOF);
}
}
}