/*
* Copyright © 2014-2016 Cask Data, Inc.
*
* 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 co.cask.cdap.logging.gateway.handlers;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.logging.LoggingContext;
import co.cask.cdap.internal.app.store.RunRecordMeta;
import co.cask.cdap.logging.LoggingConfiguration;
import co.cask.cdap.logging.context.LoggingContextHelper;
import co.cask.cdap.logging.filter.Filter;
import co.cask.cdap.logging.filter.FilterParser;
import co.cask.cdap.logging.gateway.handlers.store.ProgramStore;
import co.cask.cdap.logging.read.LogOffset;
import co.cask.cdap.logging.read.LogReader;
import co.cask.cdap.logging.read.ReadRange;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.ProgramType;
import co.cask.http.AbstractHttpHandler;
import co.cask.http.HttpHandler;
import co.cask.http.HttpResponder;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
/**
* v3 {@link HttpHandler} to handle /logs requests
*/
@Singleton
@Path(Constants.Gateway.API_VERSION_3)
public class LogHandler extends AbstractHttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(LogHandler.class);
private final LogReader logReader;
private final ProgramStore programStore;
private final String logPattern;
@Inject
public LogHandler(LogReader logReader, CConfiguration cConfig, ProgramStore programStore) {
this.logReader = logReader;
this.programStore = programStore;
this.logPattern = cConfig.get(LoggingConfiguration.LOG_PATTERN, LoggingConfiguration.DEFAULT_LOG_PATTERN);
}
@GET
@Path("/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/logs")
public void getLogs(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
@PathParam("app-id") String appId, @PathParam("program-type") String programType,
@PathParam("program-id") String programId,
@QueryParam("start") @DefaultValue("-1") long fromTimeSecsParam,
@QueryParam("stop") @DefaultValue("-1") long toTimeSecsParam,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
LoggingContext loggingContext =
LoggingContextHelper.getLoggingContext(namespaceId, appId, programId,
ProgramType.valueOfCategoryName(programType));
doGetLogs(responder, loggingContext, fromTimeSecsParam, toTimeSecsParam, escape, filterStr, null);
}
@GET
@Path("/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/runs/{run-id}/logs")
public void getRunIdLogs(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
@PathParam("app-id") String appId, @PathParam("program-type") String programType,
@PathParam("program-id") String programId, @PathParam("run-id") String runId,
@QueryParam("start") @DefaultValue("-1") long fromTimeSecsParam,
@QueryParam("stop") @DefaultValue("-1") long toTimeSecsParam,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
ProgramType type = ProgramType.valueOfCategoryName(programType);
RunRecordMeta runRecord = programStore.getRun(Id.Program.from(namespaceId, appId, type, programId), runId);
LoggingContext loggingContext = LoggingContextHelper.getLoggingContextWithRunId(namespaceId, appId, programId, type,
runId, runRecord.getSystemArgs());
doGetLogs(responder, loggingContext, fromTimeSecsParam, toTimeSecsParam, escape, filterStr, runRecord);
}
private void doGetLogs(HttpResponder responder, LoggingContext loggingContext,
long fromTimeSecsParam, long toTimeSecsParam, boolean escape, String filterStr,
@Nullable RunRecordMeta runRecord) {
try {
TimeRange timeRange = parseTime(fromTimeSecsParam, toTimeSecsParam, responder);
if (timeRange == null) {
return;
}
Filter filter = FilterParser.parse(filterStr);
ReadRange readRange = new ReadRange(timeRange.getFromMillis(), timeRange.getToMillis(),
LogOffset.INVALID_KAFKA_OFFSET);
readRange = adjustReadRange(readRange, runRecord, fromTimeSecsParam != -1);
ChunkedLogReaderCallback logCallback = new ChunkedLogReaderCallback(responder, logPattern, escape);
logReader.getLog(loggingContext, readRange.getFromMillis(), readRange.getToMillis(), filter, logCallback);
logCallback.close();
} catch (SecurityException e) {
responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
@GET
@Path("/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/logs/next")
public void next(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
@PathParam("app-id") String appId, @PathParam("program-type") String programType,
@PathParam("program-id") String programId, @QueryParam("max") @DefaultValue("50") int maxEvents,
@QueryParam("fromOffset") @DefaultValue("") String fromOffsetStr,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
LoggingContext loggingContext =
LoggingContextHelper.getLoggingContext(namespaceId, appId,
programId, ProgramType.valueOfCategoryName(programType));
doNext(responder, loggingContext, maxEvents, fromOffsetStr, escape, filterStr, null);
}
@GET
@Path("/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/runs/{run-id}/logs/next")
public void runIdNext(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
@PathParam("app-id") String appId, @PathParam("program-type") String programType,
@PathParam("program-id") String programId, @PathParam("run-id") String runId,
@QueryParam("max") @DefaultValue("50") int maxEvents,
@QueryParam("fromOffset") @DefaultValue("") String fromOffsetStr,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
ProgramType type = ProgramType.valueOfCategoryName(programType);
RunRecordMeta runRecord = programStore.getRun(Id.Program.from(namespaceId, appId, type, programId), runId);
LoggingContext loggingContext = LoggingContextHelper.getLoggingContextWithRunId(namespaceId, appId, programId, type,
runId, runRecord.getSystemArgs());
doNext(responder, loggingContext, maxEvents, fromOffsetStr, escape, filterStr, runRecord);
}
private void doNext(HttpResponder responder, LoggingContext loggingContext, int maxEvents,
String fromOffsetStr, boolean escape, String filterStr, @Nullable RunRecordMeta runRecord) {
try {
Filter filter = FilterParser.parse(filterStr);
LogReaderCallback logCallback = new LogReaderCallback(responder, logPattern, escape);
LogOffset logOffset = FormattedLogEvent.parseLogOffset(fromOffsetStr);
ReadRange readRange = ReadRange.createFromRange(logOffset);
readRange = adjustReadRange(readRange, runRecord, true);
logReader.getLogNext(loggingContext, readRange, maxEvents, filter, logCallback);
logCallback.close();
} catch (SecurityException e) {
responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
/**
* If readRange is outside runRecord's range, then the readRange is adjusted to fall within runRecords range.
*/
private ReadRange adjustReadRange(ReadRange readRange, @Nullable RunRecordMeta runRecord,
boolean fromTimeSpecified) {
if (runRecord == null) {
return readRange;
}
long fromTimeMillis = readRange.getFromMillis();
long toTimeMillis = readRange.getToMillis();
long runStartMillis = TimeUnit.SECONDS.toMillis(runRecord.getStartTs());
if (!fromTimeSpecified) {
// If from time is not specified explicitly, use the run records start time as from time
fromTimeMillis = runStartMillis;
}
if (fromTimeMillis < runStartMillis) {
// If from time is specified but is smaller than run records start time, reset it to
// run record start time. This is to optimize so that we do not look into extra files.
fromTimeMillis = runStartMillis;
}
if (runRecord.getStopTs() != null) {
// Add a buffer to stop time due to CDAP-3100
long runStopMillis = TimeUnit.SECONDS.toMillis(runRecord.getStopTs() + 1);
if (toTimeMillis > runStopMillis) {
toTimeMillis = runStopMillis;
}
}
ReadRange adjusted = new ReadRange(fromTimeMillis, toTimeMillis, readRange.getKafkaOffset());
LOG.trace("Original read range: {}. Adjusted read range: {}", readRange, adjusted);
return adjusted;
}
@GET
@Path("/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/logs/prev")
public void prev(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
@PathParam("app-id") String appId, @PathParam("program-type") String programType,
@PathParam("program-id") String programId, @QueryParam("max") @DefaultValue("50") int maxEvents,
@QueryParam("fromOffset") @DefaultValue("") String fromOffsetStr,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
LoggingContext loggingContext =
LoggingContextHelper.getLoggingContext(namespaceId, appId, programId,
ProgramType.valueOfCategoryName(programType));
doPrev(responder, loggingContext, maxEvents, fromOffsetStr, escape, filterStr, null);
}
@GET
@Path("/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/runs/{run-id}/logs/prev")
public void runIdPrev(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
@PathParam("app-id") String appId, @PathParam("program-type") String programType,
@PathParam("program-id") String programId, @PathParam("run-id") String runId,
@QueryParam("max") @DefaultValue("50") int maxEvents,
@QueryParam("fromOffset") @DefaultValue("") String fromOffsetStr,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
ProgramType type = ProgramType.valueOfCategoryName(programType);
RunRecordMeta runRecord = programStore.getRun(Id.Program.from(namespaceId, appId, type, programId), runId);
LoggingContext loggingContext = LoggingContextHelper.getLoggingContextWithRunId(namespaceId, appId, programId, type,
runId, runRecord.getSystemArgs());
doPrev(responder, loggingContext, maxEvents, fromOffsetStr, escape, filterStr, runRecord);
}
private void doPrev(HttpResponder responder, LoggingContext loggingContext, int maxEvents, String fromOffsetStr,
boolean escape, String filterStr, @Nullable RunRecordMeta runRecord) {
try {
Filter filter = FilterParser.parse(filterStr);
LogReaderCallback logCallback = new LogReaderCallback(responder, logPattern, escape);
LogOffset logOffset = FormattedLogEvent.parseLogOffset(fromOffsetStr);
ReadRange readRange = ReadRange.createToRange(logOffset);
readRange = adjustReadRange(readRange, runRecord, true);
logReader.getLogPrev(loggingContext, readRange,
maxEvents, filter, logCallback);
logCallback.close();
} catch (SecurityException e) {
responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
@GET
@Path("/system/{component-id}/{service-id}/logs")
public void sysList(HttpRequest request, HttpResponder responder, @PathParam("component-id") String componentId,
@PathParam("service-id") String serviceId,
@QueryParam("start") @DefaultValue("-1") long fromTimeSecsParam,
@QueryParam("stop") @DefaultValue("-1") long toTimeSecsParam,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
try {
TimeRange timeRange = parseTime(fromTimeSecsParam, toTimeSecsParam, responder);
if (timeRange == null) {
return;
}
Filter filter = FilterParser.parse(filterStr);
LoggingContext loggingContext = LoggingContextHelper.getLoggingContext(Id.Namespace.SYSTEM.getId(), componentId,
serviceId);
ChunkedLogReaderCallback logCallback = new ChunkedLogReaderCallback(responder, logPattern, escape);
logReader.getLog(loggingContext, timeRange.getFromMillis(), timeRange.getToMillis(), filter, logCallback);
logCallback.close();
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
@GET
@Path("/system/{component-id}/{service-id}/logs/next")
public void sysNext(HttpRequest request, HttpResponder responder, @PathParam("component-id") String componentId,
@PathParam("service-id") String serviceId, @QueryParam("max") @DefaultValue("50") int maxEvents,
@QueryParam("fromOffset") @DefaultValue("") String fromOffsetStr,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
try {
Filter filter = FilterParser.parse(filterStr);
LoggingContext loggingContext = LoggingContextHelper.getLoggingContext(Id.Namespace.SYSTEM.getId(), componentId,
serviceId);
LogReaderCallback logCallback = new LogReaderCallback(responder, logPattern, escape);
LogOffset logOffset = FormattedLogEvent.parseLogOffset(fromOffsetStr);
ReadRange readRange = ReadRange.createFromRange(logOffset);
logReader.getLogNext(loggingContext, readRange,
maxEvents, filter, logCallback);
logCallback.close();
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
@GET
@Path("/system/{component-id}/{service-id}/logs/prev")
public void sysPrev(HttpRequest request, HttpResponder responder, @PathParam("component-id") String componentId,
@PathParam("service-id") String serviceId, @QueryParam("max") @DefaultValue("50") int maxEvents,
@QueryParam("fromOffset") @DefaultValue("") String fromOffsetStr,
@QueryParam("escape") @DefaultValue("true") boolean escape,
@QueryParam("filter") @DefaultValue("") String filterStr) {
try {
Filter filter = FilterParser.parse(filterStr);
LoggingContext loggingContext = LoggingContextHelper.getLoggingContext(Id.Namespace.SYSTEM.getId(), componentId,
serviceId);
LogReaderCallback logCallback = new LogReaderCallback(responder, logPattern, escape);
LogOffset logOffset = FormattedLogEvent.parseLogOffset(fromOffsetStr);
ReadRange readRange = ReadRange.createToRange(logOffset);
logReader.getLogPrev(loggingContext, readRange,
maxEvents, filter, logCallback);
logCallback.close();
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
private static final class TimeRange {
private final long fromMillis;
private final long toMillis;
private TimeRange(long fromMillis, long toMillis) {
this.fromMillis = fromMillis;
this.toMillis = toMillis;
}
public long getFromMillis() {
return fromMillis;
}
public long getToMillis() {
return toMillis;
}
}
private static TimeRange parseTime(long fromTimeSecsParam, long toTimeSecsParam, HttpResponder responder) {
long currentTimeMillis = System.currentTimeMillis();
long fromMillis = fromTimeSecsParam < 0 ?
currentTimeMillis - TimeUnit.HOURS.toMillis(1) : TimeUnit.SECONDS.toMillis(fromTimeSecsParam);
long toMillis = toTimeSecsParam < 0 ? currentTimeMillis : TimeUnit.SECONDS.toMillis(toTimeSecsParam);
if (toMillis <= fromMillis) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, "Invalid time range. " +
"'stop' should be greater than 'start'.");
return null;
}
return new TimeRange(fromMillis, toMillis);
}
}