/*
* Copyright 2011-2016 the original author or authors.
*
* 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.glowroot.ui;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import org.immutables.value.Value;
import org.glowroot.common.live.ImmutableTracePointFilter;
import org.glowroot.common.live.LiveTraceRepository;
import org.glowroot.common.live.LiveTraceRepository.TraceKind;
import org.glowroot.common.live.LiveTraceRepository.TracePoint;
import org.glowroot.common.live.LiveTraceRepository.TracePointFilter;
import org.glowroot.common.live.StringComparator;
import org.glowroot.common.model.Result;
import org.glowroot.common.repo.ConfigRepository;
import org.glowroot.common.repo.ImmutableTraceQuery;
import org.glowroot.common.repo.TraceRepository;
import org.glowroot.common.repo.TraceRepository.TraceQuery;
import org.glowroot.common.util.Clock;
import org.glowroot.ui.TransactionJsonService.TransactionDataRequest;
import static java.util.concurrent.TimeUnit.HOURS;
@JsonService
class TracePointJsonService {
private static final int NANOSECONDS_PER_MILLISECOND = 1000000;
private static final JsonFactory jsonFactory = new JsonFactory();
private final TraceRepository traceRepository;
private final LiveTraceRepository liveTraceRepository;
private final ConfigRepository configRepository;
// null in the central ui (due to shading issue, and not needed in the central ui anyways)
private final @Nullable Ticker ticker;
private final Clock clock;
TracePointJsonService(TraceRepository traceRepository, LiveTraceRepository liveTraceRepository,
ConfigRepository configRepository, @Nullable Ticker ticker, Clock clock) {
this.traceRepository = traceRepository;
this.liveTraceRepository = liveTraceRepository;
this.configRepository = configRepository;
this.ticker = ticker;
this.clock = clock;
}
@GET(path = "/backend/transaction/trace-count", permission = "agent:transaction:traces")
String getTransactionTraceCount(@BindAgentRollupId String agentRollupId,
@BindRequest TransactionDataRequest request) throws Exception {
TraceQuery query = ImmutableTraceQuery.builder()
.transactionType(request.transactionType())
.transactionName(request.transactionName())
.from(request.from())
.to(request.to())
.build();
long traceCount = traceRepository.readSlowCount(agentRollupId, query);
boolean includeActiveTraces = shouldIncludeActiveTraces(request);
if (includeActiveTraces) {
traceCount += liveTraceRepository.getMatchingTraceCount(request.transactionType(),
request.transactionName());
}
return Long.toString(traceCount);
}
@GET(path = "/backend/error/trace-count", permission = "agent:error:traces")
String getErrorTraceCount(@BindAgentRollupId String agentRollupId,
@BindRequest TraceQuery query) throws Exception {
return Long.toString(traceRepository.readErrorCount(agentRollupId, query));
}
@GET(path = "/backend/transaction/points", permission = "agent:transaction:traces")
String getTransactionPoints(@BindAgentRollupId String agentRollupId,
@BindRequest TracePointRequest request) throws Exception {
return getPoints(TraceKind.SLOW, agentRollupId, request);
}
@GET(path = "/backend/error/points", permission = "agent:error:traces")
String getErrorPoints(@BindAgentRollupId String agentRollupId,
@BindRequest TracePointRequest request) throws Exception {
return getPoints(TraceKind.ERROR, agentRollupId, request);
}
private boolean shouldIncludeActiveTraces(TransactionDataRequest request) {
long currentTimeMillis = clock.currentTimeMillis();
return (request.to() == 0 || request.to() > currentTimeMillis)
&& request.from() < currentTimeMillis;
}
private String getPoints(TraceKind traceKind, String agentRollupId, TracePointRequest request)
throws Exception {
TraceQuery query = ImmutableTraceQuery.builder()
.transactionType(request.transactionType())
.transactionName(request.transactionName())
.from(request.from())
.to(request.to())
.build();
TracePointFilter filter = ImmutableTracePointFilter.builder()
.headlineComparator(request.headlineComparator())
.headline(request.headline())
.errorMessageComparator(request.errorMessageComparator())
.errorMessage(request.errorMessage())
.userComparator(request.userComparator())
.user(request.user())
.attributeName(request.attributeName())
.attributeValueComparator(request.attributeValueComparator())
.attributeValue(request.attributeValue())
.build();
return new Handler(traceKind, agentRollupId, query, filter, request.limit()).handle();
}
private class Handler {
private final TraceKind traceKind;
private final String agentRollupId;
private final TraceQuery query;
private final TracePointFilter filter;
private final int limit;
private Handler(TraceKind traceKind, String agentRollupId, TraceQuery query,
TracePointFilter filter, int limit) {
this.traceKind = traceKind;
this.agentRollupId = agentRollupId;
this.query = query;
this.filter = filter;
this.limit = limit;
}
private String handle() throws Exception {
boolean captureActiveTracePoints = shouldCaptureActiveTracePoints();
List<TracePoint> activeTracePoints = Lists.newArrayList();
long captureTime = 0;
long captureTick = 0;
if (captureActiveTracePoints && ticker != null) {
captureTime = clock.currentTimeMillis();
captureTick = ticker.read();
// capture active traces first to make sure that none are missed in the transition
// between active and pending/stored (possible duplicates are removed below)
activeTracePoints.addAll(liveTraceRepository.getMatchingActiveTracePoints(traceKind,
query.transactionType(), query.transactionName(), filter, limit,
captureTime, captureTick));
}
Result<TracePoint> queryResult =
getStoredAndPendingPoints(captureTime, captureActiveTracePoints);
List<TracePoint> points = Lists.newArrayList(queryResult.records());
removeDuplicatesBetweenActiveAndNormalTracePoints(activeTracePoints, points);
int traceExpirationHours = configRepository.getStorageConfig().traceExpirationHours();
boolean expired = points.isEmpty() && traceExpirationHours != 0 && query
.to() < clock.currentTimeMillis() - HOURS.toMillis(traceExpirationHours);
return writeResponse(points, activeTracePoints, queryResult.moreAvailable(), expired);
}
private boolean shouldCaptureActiveTracePoints() {
long currentTimeMillis = clock.currentTimeMillis();
return (query.to() == 0 || query.to() > currentTimeMillis)
&& query.from() < currentTimeMillis;
}
private Result<TracePoint> getStoredAndPendingPoints(long captureTime,
boolean captureActiveTraces) throws Exception {
List<TracePoint> matchingPendingPoints;
// it only seems worth looking at pending traces if request asks for active traces
if (captureActiveTraces) {
// important to grab pending traces before stored points to ensure none are
// missed in the transition between pending and stored
matchingPendingPoints = liveTraceRepository.getMatchingPendingPoints(traceKind,
query.transactionType(), query.transactionName(), filter, captureTime);
} else {
matchingPendingPoints = ImmutableList.of();
}
Result<TracePoint> queryResult;
if (traceKind == TraceKind.SLOW) {
queryResult = traceRepository.readSlowPoints(agentRollupId, query, filter, limit);
} else {
// TraceKind.ERROR
queryResult = traceRepository.readErrorPoints(agentRollupId, query, filter, limit);
}
// create single merged and limited list of points
List<TracePoint> orderedPoints = Lists.newArrayList(queryResult.records());
for (TracePoint pendingPoint : matchingPendingPoints) {
insertIntoOrderedPoints(pendingPoint, orderedPoints);
}
return new Result<TracePoint>(orderedPoints, queryResult.moreAvailable());
}
private void insertIntoOrderedPoints(TracePoint pendingPoint,
List<TracePoint> orderedPoints) {
int duplicateIndex = -1;
int insertionIndex = -1;
// check if duplicate and capture insertion index at the same time
for (int i = 0; i < orderedPoints.size(); i++) {
TracePoint point = orderedPoints.get(i);
if (pendingPoint.traceId().equals(point.traceId())) {
duplicateIndex = i;
break;
}
if (pendingPoint.durationNanos() > point.durationNanos()) {
insertionIndex = i;
break;
}
}
if (duplicateIndex != -1) {
TracePoint point = orderedPoints.get(duplicateIndex);
if (pendingPoint.durationNanos() > point.durationNanos()) {
// prefer the pending trace, it must be a partial trace that has just completed
orderedPoints.set(duplicateIndex, pendingPoint);
}
return;
}
if (insertionIndex == -1) {
orderedPoints.add(pendingPoint);
} else {
orderedPoints.add(insertionIndex, pendingPoint);
}
}
private void removeDuplicatesBetweenActiveAndNormalTracePoints(
List<TracePoint> activeTracePoints, List<TracePoint> points) {
for (Iterator<TracePoint> i = activeTracePoints.iterator(); i.hasNext();) {
TracePoint activeTracePoint = i.next();
for (Iterator<TracePoint> j = points.iterator(); j.hasNext();) {
TracePoint point = j.next();
if (!activeTracePoint.traceId().equals(point.traceId())) {
continue;
}
if (activeTracePoint.durationNanos() > point.durationNanos()) {
// prefer the active trace, it must be a partial trace that hasn't
// completed yet
j.remove();
} else {
// otherwise prefer the completed trace
i.remove();
}
// there can be at most one duplicate per id, so ok to break to outer
break;
}
}
}
private String writeResponse(List<TracePoint> points, List<TracePoint> activePoints,
boolean limitExceeded, boolean expired) throws Exception {
StringBuilder sb = new StringBuilder();
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
jg.writeArrayFieldStart("normalPoints");
for (TracePoint point : points) {
if (!point.error() && !point.partial()) {
writePoint(point, jg);
}
}
jg.writeEndArray();
jg.writeArrayFieldStart("errorPoints");
for (TracePoint point : points) {
if (point.error() && !point.partial()) {
writePoint(point, jg);
}
}
jg.writeEndArray();
jg.writeArrayFieldStart("partialPoints");
for (TracePoint point : points) {
if (point.partial()) {
writePoint(point, jg);
}
}
for (TracePoint activePoint : activePoints) {
writePoint(activePoint, jg);
}
jg.writeEndArray();
if (limitExceeded) {
jg.writeBooleanField("limitExceeded", true);
}
if (expired) {
jg.writeBooleanField("expired", true);
}
jg.writeEndObject();
jg.close();
return sb.toString();
}
private void writePoint(TracePoint point, JsonGenerator jg) throws IOException {
jg.writeStartArray();
jg.writeNumber(point.captureTime());
jg.writeNumber(point.durationNanos() / NANOSECONDS_PER_MILLISECOND);
jg.writeString(point.agentId());
jg.writeString(point.traceId());
jg.writeEndArray();
}
}
// same as TracePointQuery but with milliseconds instead of nanoseconds
@Value.Immutable
public abstract static class TracePointRequest {
public abstract String transactionType();
public abstract @Nullable String transactionName();
public abstract long from();
public abstract long to();
public abstract @Nullable StringComparator headlineComparator();
public abstract @Nullable String headline();
public abstract @Nullable StringComparator errorMessageComparator();
public abstract @Nullable String errorMessage();
public abstract @Nullable StringComparator userComparator();
public abstract @Nullable String user();
public abstract @Nullable String attributeName();
public abstract @Nullable StringComparator attributeValueComparator();
public abstract @Nullable String attributeValue();
public abstract int limit();
}
}