/*
* Copyright 2012-2017 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.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import javax.annotation.Nullable;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import com.google.common.io.CharStreams;
import org.immutables.value.Value;
import org.glowroot.common.live.LiveTraceRepository;
import org.glowroot.common.live.LiveTraceRepository.Entries;
import org.glowroot.common.live.LiveTraceRepository.Existence;
import org.glowroot.common.model.MutableProfile;
import org.glowroot.common.repo.AgentRepository;
import org.glowroot.common.repo.TraceRepository;
import org.glowroot.common.repo.TraceRepository.HeaderPlus;
import org.glowroot.common.util.Styles;
import org.glowroot.wire.api.model.ProfileOuterClass.Profile;
import org.glowroot.wire.api.model.Proto;
import org.glowroot.wire.api.model.TraceOuterClass.Trace;
class TraceCommonService {
private static final JsonFactory jsonFactory = new JsonFactory();
private final TraceRepository traceRepository;
private final LiveTraceRepository liveTraceRepository;
private final AgentRepository agentRepository;
TraceCommonService(TraceRepository traceRepository, LiveTraceRepository liveTraceRepository,
AgentRepository agentRepository) {
this.traceRepository = traceRepository;
this.liveTraceRepository = liveTraceRepository;
this.agentRepository = agentRepository;
}
@Nullable
String getHeaderJson(String agentRollupId, String agentId, String traceId,
boolean checkLiveTraces) throws Exception {
if (checkLiveTraces) {
// check active/pending traces first, and lastly stored traces to make sure that the
// trace is not missed if it is in transition between these states
Trace.Header header = liveTraceRepository.getHeader(agentRollupId, agentId, traceId);
if (header != null) {
return toJsonLiveHeader(agentId, header);
}
}
HeaderPlus header = getStoredHeader(agentRollupId, agentId, traceId,
new RetryCountdown(checkLiveTraces));
if (header == null) {
return null;
}
return toJsonRepoHeader(agentId, header);
}
// TODO this comment is no longer valid?
// overwritten entries will return {"overwritten":true}
// expired (not found) trace will return {"expired":true}
@Nullable
String getEntriesJson(String agentRollupId, String agentId, String traceId,
boolean checkLiveTraces) throws Exception {
if (checkLiveTraces) {
// check active/pending traces first, and lastly stored traces to make sure that the
// trace is not missed if it is in transition between these states
Entries entries = liveTraceRepository.getEntries(agentRollupId, agentId, traceId);
if (entries != null) {
return toJson(entries);
}
}
return toJson(getStoredEntries(agentRollupId, agentId, traceId,
new RetryCountdown(checkLiveTraces)));
}
// overwritten profile will return {"overwritten":true}
// expired (not found) trace will return {"expired":true}
@Nullable
String getMainThreadProfileJson(String agentRollupId, String agentId, String traceId,
boolean checkLiveTraces) throws Exception {
if (checkLiveTraces) {
// check active/pending traces first, and lastly stored traces to make sure that the
// trace is not missed if it is in transition between these states
Profile profile =
liveTraceRepository.getMainThreadProfile(agentRollupId, agentId, traceId);
if (profile != null) {
return toJson(profile);
}
}
return toJson(getStoredMainThreadProfile(agentRollupId, agentId, traceId,
new RetryCountdown(checkLiveTraces)));
}
// overwritten profile will return {"overwritten":true}
// expired (not found) trace will return {"expired":true}
@Nullable
String getAuxThreadProfileJson(String agentRollupId, String agentId, String traceId,
boolean checkLiveTraces) throws Exception {
if (checkLiveTraces) {
// check active/pending traces first, and lastly stored traces to make sure that the
// trace is not missed if it is in transition between these states
Profile profile =
liveTraceRepository.getAuxThreadProfile(agentRollupId, agentId, traceId);
if (profile != null) {
return toJson(profile);
}
}
return toJson(getStoredAuxThreadProfile(agentRollupId, agentId, traceId,
new RetryCountdown(checkLiveTraces)));
}
@Nullable
TraceExport getExport(String agentRollupId, String agentId, String traceId,
boolean checkLiveTraces) throws Exception {
if (checkLiveTraces) {
// check active/pending traces first, and lastly stored traces to make sure that the
// trace is not missed if it is in transition between these states
Trace trace = liveTraceRepository.getFullTrace(agentRollupId, agentId, traceId);
if (trace != null) {
Trace.Header header = trace.getHeader();
return ImmutableTraceExport.builder()
.fileName(getFileName(header))
.headerJson(toJsonLiveHeader(agentId, header))
.entriesJson(entriesToJson(trace.getEntryList()))
// SharedQueryTexts are always returned from getFullTrace() above with
// fullTrace, so no need to resolve fullTraceSha1
.sharedQueryTextsJson(
sharedQueryTextsToJson(trace.getSharedQueryTextList()))
.mainThreadProfileJson(toJson(trace.getMainThreadProfile()))
.auxThreadProfileJson(toJson(trace.getAuxThreadProfile()))
.build();
}
}
RetryCountdown retryCountdown = new RetryCountdown(checkLiveTraces);
HeaderPlus header = getStoredHeader(agentRollupId, agentId, traceId, retryCountdown);
if (header == null) {
return null;
}
ImmutableTraceExport.Builder builder = ImmutableTraceExport.builder()
.fileName(getFileName(header.header()))
.headerJson(toJsonRepoHeader(agentId, header));
Entries entries =
getStoredEntriesForExport(agentRollupId, agentId, traceId, retryCountdown);
if (entries != null) {
builder.entriesJson(entriesToJson(entries.entries()));
// SharedQueryTexts are always returned from getStoredEntries() above with fullTrace,
// so no need to resolve fullTraceSha1
builder.sharedQueryTextsJson(sharedQueryTextsToJson(entries.sharedQueryTexts()));
}
builder.mainThreadProfileJson(
toJson(getStoredMainThreadProfile(agentRollupId, agentId, traceId,
retryCountdown)));
builder.auxThreadProfileJson(
toJson(getStoredAuxThreadProfile(agentRollupId, agentId, traceId, retryCountdown)));
return builder.build();
}
private @Nullable HeaderPlus getStoredHeader(String agentRollupId, String agentId,
String traceId, RetryCountdown retryCountdown) throws Exception {
HeaderPlus headerPlus = traceRepository.readHeaderPlus(agentRollupId, agentId, traceId);
while (headerPlus == null && retryCountdown.remaining-- > 0) {
// trace may be completed, but still in transit from agent to the central collector
Thread.sleep(500);
headerPlus = traceRepository.readHeaderPlus(agentRollupId, agentId, traceId);
}
return headerPlus;
}
private @Nullable Entries getStoredEntries(String agentRollupId, String agentId, String traceId,
RetryCountdown retryCountdown) throws Exception {
Entries entries = traceRepository.readEntries(agentRollupId, agentId, traceId);
while (entries == null && retryCountdown.remaining-- > 0) {
// trace may be completed, but still in transit from agent to the central collector
Thread.sleep(500);
entries = traceRepository.readEntries(agentRollupId, agentId, traceId);
}
return entries;
}
private @Nullable Entries getStoredEntriesForExport(String agentRollupId, String agentId,
String traceId, RetryCountdown retryCountdown) throws Exception {
Entries entries = traceRepository.readEntriesForExport(agentRollupId, agentId, traceId);
while (entries == null && retryCountdown.remaining-- > 0) {
// trace may be completed, but still in transit from agent to the central collector
Thread.sleep(500);
entries = traceRepository.readEntriesForExport(agentRollupId, agentId, traceId);
}
return entries;
}
private @Nullable Profile getStoredMainThreadProfile(String agentRollupId, String agentId,
String traceId, RetryCountdown retryCountdown) throws Exception {
Profile profile = traceRepository.readMainThreadProfile(agentRollupId, agentId, traceId);
while (profile == null && retryCountdown.remaining-- > 0) {
// trace may be completed, but still in transit from agent to the central collector
Thread.sleep(500);
profile = traceRepository.readMainThreadProfile(agentRollupId, agentId, traceId);
}
return profile;
}
private @Nullable Profile getStoredAuxThreadProfile(String agentRollupId, String agentId,
String traceId, RetryCountdown retryCountdown) throws Exception {
Profile profile = traceRepository.readAuxThreadProfile(agentRollupId, agentId, traceId);
while (profile == null && retryCountdown.remaining-- > 0) {
// trace may be completed, but still in transit from agent to the central collector
Thread.sleep(500);
profile = traceRepository.readAuxThreadProfile(agentRollupId, agentId, traceId);
}
return profile;
}
private static @Nullable String toJson(@Nullable Entries entries) throws IOException {
if (entries == null) {
return null;
}
StringBuilder sb = new StringBuilder();
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
jg.writeFieldName("entries");
writeEntries(jg, entries.entries());
jg.writeFieldName("sharedQueryTexts");
writeSharedQueryTexts(jg, entries.sharedQueryTexts());
jg.writeEndObject();
jg.close();
return sb.toString();
}
@VisibleForTesting
static @Nullable String entriesToJson(List<Trace.Entry> entries) throws IOException {
if (entries.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
writeEntries(jg, entries);
jg.close();
return sb.toString();
}
private static @Nullable String sharedQueryTextsToJson(
List<Trace.SharedQueryText> sharedQueryTexts) throws IOException {
if (sharedQueryTexts.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
writeSharedQueryTexts(jg, sharedQueryTexts);
jg.close();
return sb.toString();
}
private static void writeEntries(JsonGenerator jg, List<Trace.Entry> entries)
throws IOException {
jg.writeStartArray();
PeekingIterator<Trace.Entry> i = Iterators.peekingIterator(entries.iterator());
while (i.hasNext()) {
Trace.Entry entry = i.next();
int depth = entry.getDepth();
jg.writeStartObject();
writeJson(entry, jg);
int nextDepth = i.hasNext() ? i.peek().getDepth() : 0;
if (nextDepth > depth) {
jg.writeArrayFieldStart("childEntries");
} else if (nextDepth < depth) {
jg.writeEndObject();
for (int j = depth; j > nextDepth; j--) {
jg.writeEndArray();
jg.writeEndObject();
}
} else {
jg.writeEndObject();
}
}
jg.writeEndArray();
}
private static void writeSharedQueryTexts(JsonGenerator jg,
List<Trace.SharedQueryText> sharedQueryTexts) throws IOException {
jg.writeStartArray();
for (Trace.SharedQueryText sharedQueryText : sharedQueryTexts) {
jg.writeStartObject();
String fullText = sharedQueryText.getFullText();
if (fullText.isEmpty()) {
// truncatedText, truncatedEndText and fullTextSha1 are all provided in this case
jg.writeStringField("truncatedText", sharedQueryText.getTruncatedText());
jg.writeStringField("truncatedEndText", sharedQueryText.getTruncatedEndText());
jg.writeStringField("fullTextSha1", sharedQueryText.getFullTextSha1());
} else {
jg.writeStringField("fullText", fullText);
}
jg.writeEndObject();
}
jg.writeEndArray();
}
private static @Nullable String toJson(@Nullable Profile profile) throws IOException {
if (profile == null) {
return null;
}
MutableProfile mutableProfile = new MutableProfile();
mutableProfile.merge(profile);
return mutableProfile.toJson();
}
private String toJsonLiveHeader(String agentId, Trace.Header header) throws Exception {
boolean hasProfile = header.getMainThreadProfileSampleCount() > 0
|| header.getAuxThreadProfileSampleCount() > 0;
return toJson(agentId, header, header.getPartial(),
header.getEntryCount() > 0 ? Existence.YES : Existence.NO,
hasProfile ? Existence.YES : Existence.NO);
}
private String toJsonRepoHeader(String agentId, HeaderPlus header) throws Exception {
return toJson(agentId, header.header(), false, header.entriesExistence(),
header.profileExistence());
}
private String toJson(String agentId, Trace.Header header, boolean active,
Existence entriesExistence, Existence profileExistence) throws Exception {
StringBuilder sb = new StringBuilder();
JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
if (!agentId.isEmpty()) {
jg.writeStringField("agent", agentRepository.readAgentRollupDisplay(agentId));
}
if (active) {
jg.writeBooleanField("active", active);
}
boolean partial = header.getPartial();
if (partial) {
jg.writeBooleanField("partial", partial);
}
boolean async = header.getAsync();
if (async) {
jg.writeBooleanField("async", async);
}
jg.writeNumberField("startTime", header.getStartTime());
jg.writeNumberField("captureTime", header.getCaptureTime());
jg.writeNumberField("durationNanos", header.getDurationNanos());
jg.writeStringField("transactionType", header.getTransactionType());
jg.writeStringField("transactionName", header.getTransactionName());
jg.writeStringField("headline", header.getHeadline());
jg.writeStringField("user", header.getUser());
List<Trace.Attribute> attributes = header.getAttributeList();
if (!attributes.isEmpty()) {
jg.writeObjectFieldStart("attributes");
for (Trace.Attribute attribute : attributes) {
jg.writeArrayFieldStart(attribute.getName());
for (String value : attribute.getValueList()) {
jg.writeString(value);
}
jg.writeEndArray();
}
jg.writeEndObject();
}
List<Trace.DetailEntry> detailEntries = header.getDetailEntryList();
if (!detailEntries.isEmpty()) {
jg.writeFieldName("detail");
writeDetailEntries(detailEntries, jg);
}
if (header.hasError()) {
jg.writeFieldName("error");
writeError(header.getError(), jg);
}
if (header.hasMainThreadRootTimer()) {
jg.writeFieldName("mainThreadRootTimer");
writeTimer(header.getMainThreadRootTimer(), jg);
}
jg.writeArrayFieldStart("auxThreadRootTimers");
for (Trace.Timer rootTimer : header.getAuxThreadRootTimerList()) {
writeTimer(rootTimer, jg);
}
jg.writeEndArray();
jg.writeArrayFieldStart("asyncTimers");
for (Trace.Timer asyncTimer : header.getAsyncTimerList()) {
writeTimer(asyncTimer, jg);
}
jg.writeEndArray();
if (header.hasMainThreadStats()) {
jg.writeFieldName("mainThreadStats");
writeThreadStats(header.getMainThreadStats(), jg);
}
if (header.hasAuxThreadStats()) {
jg.writeFieldName("auxThreadStats");
writeThreadStats(header.getAuxThreadStats(), jg);
}
jg.writeNumberField("entryCount", header.getEntryCount());
boolean entryLimitExceeded = header.getEntryLimitExceeded();
if (entryLimitExceeded) {
jg.writeBooleanField("entryLimitExceeded", entryLimitExceeded);
}
jg.writeNumberField("mainThreadProfileSampleCount",
header.getMainThreadProfileSampleCount());
boolean mainThreadProfileSampleLimitExceeded =
header.getMainThreadProfileSampleLimitExceeded();
if (mainThreadProfileSampleLimitExceeded) {
jg.writeBooleanField("mainThreadProfileSampleLimitExceeded",
mainThreadProfileSampleLimitExceeded);
}
jg.writeNumberField("auxThreadProfileSampleCount", header.getAuxThreadProfileSampleCount());
boolean auxThreadProfileSampleLimitExceeded =
header.getAuxThreadProfileSampleLimitExceeded();
if (auxThreadProfileSampleLimitExceeded) {
jg.writeBooleanField("auxThreadProfileSampleLimitExceeded",
auxThreadProfileSampleLimitExceeded);
}
jg.writeStringField("entriesExistence",
entriesExistence.name().toLowerCase(Locale.ENGLISH));
jg.writeStringField("profileExistence",
profileExistence.name().toLowerCase(Locale.ENGLISH));
jg.writeEndObject();
jg.close();
return sb.toString();
}
private static void writeJson(Trace.Entry entry, JsonGenerator jg) throws IOException {
jg.writeNumberField("startOffsetNanos", entry.getStartOffsetNanos());
jg.writeNumberField("durationNanos", entry.getDurationNanos());
if (entry.getActive()) {
jg.writeBooleanField("active", true);
}
if (entry.hasQueryEntryMessage()) {
jg.writeObjectFieldStart("queryMessage");
Trace.QueryEntryMessage queryMessage = entry.getQueryEntryMessage();
jg.writeNumberField("sharedQueryTextIndex", queryMessage.getSharedQueryTextIndex());
jg.writeStringField("prefix", queryMessage.getPrefix());
jg.writeStringField("suffix", queryMessage.getSuffix());
jg.writeEndObject();
} else {
jg.writeStringField("message", entry.getMessage());
}
List<Trace.DetailEntry> detailEntries = entry.getDetailEntryList();
if (!detailEntries.isEmpty()) {
jg.writeFieldName("detail");
writeDetailEntries(detailEntries, jg);
}
List<Proto.StackTraceElement> locationStackTraceElements =
entry.getLocationStackTraceElementList();
if (!locationStackTraceElements.isEmpty()) {
jg.writeArrayFieldStart("locationStackTraceElements");
for (Proto.StackTraceElement stackTraceElement : locationStackTraceElements) {
writeStackTraceElement(stackTraceElement, jg);
}
jg.writeEndArray();
}
if (entry.hasError()) {
jg.writeFieldName("error");
writeError(entry.getError(), jg);
}
}
private static void writeDetailEntries(List<Trace.DetailEntry> detailEntries, JsonGenerator jg)
throws IOException {
jg.writeStartObject();
for (Trace.DetailEntry detailEntry : detailEntries) {
jg.writeFieldName(detailEntry.getName());
List<Trace.DetailEntry> childEntries = detailEntry.getChildEntryList();
List<Trace.DetailValue> values = detailEntry.getValueList();
if (!childEntries.isEmpty()) {
writeDetailEntries(childEntries, jg);
} else if (values.size() == 1) {
writeValue(values.get(0), jg);
} else if (values.size() > 1) {
jg.writeStartArray();
for (Trace.DetailValue value : values) {
writeValue(value, jg);
}
jg.writeEndArray();
} else {
jg.writeNull();
}
}
jg.writeEndObject();
}
private static void writeValue(Trace.DetailValue value, JsonGenerator jg) throws IOException {
switch (value.getValCase()) {
case STRING:
jg.writeString(value.getString());
break;
case DOUBLE:
jg.writeNumber(value.getDouble());
break;
case LONG:
jg.writeNumber(value.getLong());
break;
case BOOLEAN:
jg.writeBoolean(value.getBoolean());
break;
default:
throw new IllegalStateException("Unexpected detail value: " + value.getValCase());
}
}
private static void writeError(Trace.Error error, JsonGenerator jg) throws IOException {
jg.writeStartObject();
jg.writeStringField("message", error.getMessage());
if (error.hasException()) {
jg.writeFieldName("exception");
writeThrowable(error.getException(), false, jg);
}
jg.writeEndObject();
}
private static void writeThrowable(Proto.Throwable throwable, boolean hasEnclosing,
JsonGenerator jg) throws IOException {
jg.writeStartObject();
jg.writeStringField("className", throwable.getClassName());
jg.writeStringField("message", throwable.getMessage());
jg.writeArrayFieldStart("stackTraceElements");
for (Proto.StackTraceElement stackTraceElement : throwable.getStackTraceElementList()) {
writeStackTraceElement(stackTraceElement, jg);
}
jg.writeEndArray();
if (hasEnclosing) {
jg.writeNumberField("framesInCommonWithEnclosing",
throwable.getFramesInCommonWithEnclosing());
}
if (throwable.hasCause()) {
jg.writeFieldName("cause");
writeThrowable(throwable.getCause(), true, jg);
}
jg.writeEndObject();
}
private static void writeTimer(Trace.Timer timer, JsonGenerator jg) throws IOException {
jg.writeStartObject();
jg.writeStringField("name", timer.getName());
boolean extended = timer.getExtended();
if (extended) {
jg.writeBooleanField("extended", extended);
}
jg.writeNumberField("totalNanos", timer.getTotalNanos());
jg.writeNumberField("count", timer.getCount());
boolean active = timer.getActive();
if (active) {
jg.writeBooleanField("active", active);
}
List<Trace.Timer> childTimers = timer.getChildTimerList();
if (!childTimers.isEmpty()) {
jg.writeArrayFieldStart("childTimers");
for (Trace.Timer childTimer : childTimers) {
writeTimer(childTimer, jg);
}
jg.writeEndArray();
}
jg.writeEndObject();
}
private static void writeThreadStats(Trace.ThreadStats threadStats, JsonGenerator jg)
throws IOException {
jg.writeStartObject();
if (threadStats.hasTotalCpuNanos()) {
jg.writeNumberField("totalCpuNanos", threadStats.getTotalCpuNanos().getValue());
}
if (threadStats.hasTotalBlockedNanos()) {
jg.writeNumberField("totalBlockedNanos", threadStats.getTotalBlockedNanos().getValue());
}
if (threadStats.hasTotalWaitedNanos()) {
jg.writeNumberField("totalWaitedNanos", threadStats.getTotalWaitedNanos().getValue());
}
if (threadStats.hasTotalAllocatedBytes()) {
jg.writeNumberField("totalAllocatedBytes",
threadStats.getTotalAllocatedBytes().getValue());
}
jg.writeEndObject();
}
private static void writeStackTraceElement(Proto.StackTraceElement stackTraceElement,
JsonGenerator jg) throws IOException {
jg.writeString(new StackTraceElement(stackTraceElement.getClassName(),
stackTraceElement.getMethodName(), stackTraceElement.getFileName(),
stackTraceElement.getLineNumber()).toString());
}
private static String getFileName(Trace.Header header) {
return "trace-" + new SimpleDateFormat("yyyyMMdd-HHmmss-SSS").format(header.getStartTime());
}
private static class RetryCountdown {
private int remaining;
public RetryCountdown(boolean checkLiveTraces) {
remaining = checkLiveTraces ? 5 : 0;
}
}
@Value.Immutable
@Styles.AllParameters
interface TraceExport {
String fileName();
String headerJson();
@Nullable
String entriesJson();
@Nullable
String sharedQueryTextsJson();
@Nullable
String mainThreadProfileJson();
@Nullable
String auxThreadProfileJson();
}
}