/*
* Copyright 2015-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.agent.central;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glowroot.agent.central.CentralConnection.GrpcCall;
import org.glowroot.agent.collector.Collector;
import org.glowroot.agent.live.LiveJvmServiceImpl;
import org.glowroot.agent.live.LiveTraceRepositoryImpl;
import org.glowroot.agent.live.LiveWeavingServiceImpl;
import org.glowroot.common.util.OnlyUsedByTests;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig;
import org.glowroot.wire.api.model.AggregateOuterClass.Aggregate;
import org.glowroot.wire.api.model.CollectorServiceGrpc;
import org.glowroot.wire.api.model.CollectorServiceGrpc.CollectorServiceStub;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.AggregateResponseMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.AggregateStreamHeader;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.AggregateStreamMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.EmptyMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.Environment;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValue;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValueMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.InitMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.InitResponse;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.LogEvent;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.LogEvent.Level;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.LogMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.OverallAggregate;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.TraceStreamCounts;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.TraceStreamHeader;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.TraceStreamMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.TransactionAggregate;
import org.glowroot.wire.api.model.ProfileOuterClass.Profile;
import org.glowroot.wire.api.model.TraceOuterClass.Trace;
public class CentralCollector implements Collector {
private static final Logger logger = LoggerFactory.getLogger(CentralCollector.class);
// log startup messages using logger name "org.glowroot"
private static final Logger startupLogger = LoggerFactory.getLogger("org.glowroot");
private final String agentId;
private final String agentRollupId;
private final String collectorAddress;
private final CentralConnection centralConnection;
private final CollectorServiceStub collectorServiceStub;
private final DownstreamServiceObserver downstreamServiceObserver;
private final SharedQueryTextLimiter sharedQueryTextLimiter = new SharedQueryTextLimiter();
private volatile int nextAggregateDelayMillis;
public CentralCollector(Map<String, String> properties, String collectorAddress,
LiveJvmServiceImpl liveJvmService, LiveWeavingServiceImpl liveWeavingService,
LiveTraceRepositoryImpl liveTraceRepository, AgentConfigUpdater agentConfigUpdater)
throws Exception {
String agentId = properties.get("glowroot.agent.id");
if (Strings.isNullOrEmpty(agentId)) {
agentId = InetAddress.getLocalHost().getHostName();
}
this.agentId = agentId;
this.agentRollupId = Strings.nullToEmpty(properties.get("glowroot.agent.rollup.id"));
this.collectorAddress = collectorAddress;
if (agentRollupId.isEmpty()) {
startupLogger.info("agent id: {}", agentId);
} else {
startupLogger.info("agent id: {}, rollup id: {}", agentId, agentRollupId);
}
List<SocketAddress> collectorAddresses = Lists.newArrayList();
for (String addr : Splitter.on(',').trimResults().omitEmptyStrings()
.split(collectorAddress)) {
int index = addr.indexOf(':');
if (index == -1) {
throw new IllegalStateException(
"Invalid collector.address (missing port): " + addr);
}
String host = addr.substring(0, index);
int port;
try {
port = Integer.parseInt(addr.substring(index + 1));
} catch (NumberFormatException e) {
logger.debug(e.getMessage(), e);
throw new IllegalStateException(
"Invalid collector.address (invalid port): " + addr);
}
collectorAddresses.add(new InetSocketAddress(host, port));
}
AtomicBoolean inConnectionFailure = new AtomicBoolean();
centralConnection = new CentralConnection(collectorAddresses, inConnectionFailure);
collectorServiceStub = CollectorServiceGrpc.newStub(centralConnection.getChannel())
.withCompression("gzip");
downstreamServiceObserver = new DownstreamServiceObserver(centralConnection,
agentConfigUpdater, liveJvmService, liveWeavingService, liveTraceRepository,
agentId, inConnectionFailure, sharedQueryTextLimiter);
}
@Override
public void init(File glowrootDir, File agentDir, Environment environment,
AgentConfig agentConfig, final AgentConfigUpdater agentConfigUpdater) {
final InitMessage initMessage = InitMessage.newBuilder()
.setAgentId(agentId)
.setAgentRollupId(agentRollupId)
.setEnvironment(environment)
.setAgentConfig(agentConfig)
.build();
centralConnection.callInit(new GrpcCall<InitResponse>() {
@Override
public void call(StreamObserver<InitResponse> responseObserver) {
collectorServiceStub.collectInit(initMessage, responseObserver);
}
@Override
void doWithResponse(final InitResponse response) {
// don't need to suppress sending this log message to the central collector because
// startup logger info messages are never sent to the central collector
startupLogger.info("connected to the central collector {}, version {}",
collectorAddress, response.getGlowrootCentralVersion());
if (response.hasAgentConfig()) {
try {
agentConfigUpdater.update(response.getAgentConfig());
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
downstreamServiceObserver.connectAsync();
}
});
}
// collecting even when no aggregates since collection triggers transaction-based alerts
@Override
public void collectAggregates(AggregateReader aggregateReader) {
centralConnection.callWithAFewRetries(nextAggregateDelayMillis,
new CollectAggregatesGrpcCall(aggregateReader));
}
@Override
public void collectGaugeValues(List<GaugeValue> gaugeValues) {
final GaugeValueMessage gaugeValueMessage = GaugeValueMessage.newBuilder()
.setAgentId(agentId)
.addAllGaugeValues(gaugeValues)
.build();
centralConnection.callWithAFewRetries(new GrpcCall<EmptyMessage>() {
@Override
public void call(StreamObserver<EmptyMessage> responseObserver) {
collectorServiceStub.collectGaugeValues(gaugeValueMessage, responseObserver);
}
});
}
@Override
public void collectTrace(TraceReader traceReader) {
if (traceReader.partial()) {
// do not retry partial transactions since they are live and reading from the trace
// reader will not be idempotent, so could lead to confusing results
centralConnection.callOnce(new CollectTraceGrpcCall(traceReader));
} else {
centralConnection.callWithAFewRetries(new CollectTraceGrpcCall(traceReader));
}
}
@Override
public void log(LogEvent logEvent) {
if (centralConnection.suppressLogCollector()) {
return;
}
if (logEvent.getLoggerName().equals("org.glowroot") && logEvent.getLevel() == Level.INFO) {
// never send startup logger info messages to the central collector
return;
}
final LogMessage logMessage = LogMessage.newBuilder()
.setAgentId(agentId)
.setLogEvent(logEvent)
.build();
centralConnection.callWithAFewRetries(new GrpcCall<EmptyMessage>() {
@Override
public void call(StreamObserver<EmptyMessage> responseObserver) {
collectorServiceStub.log(logMessage, responseObserver);
}
});
}
@OnlyUsedByTests
public void close() throws InterruptedException {
downstreamServiceObserver.close();
centralConnection.close();
}
@OnlyUsedByTests
public void awaitClose() throws InterruptedException {
centralConnection.awaitClose();
}
private class CollectAggregatesGrpcCall extends GrpcCall<AggregateResponseMessage> {
private class AggregateVisitorImpl implements AggregateVisitor {
private final StreamObserver<AggregateStreamMessage> requestObserver;
private AggregateVisitorImpl(StreamObserver<AggregateStreamMessage> requestObserver) {
this.requestObserver = requestObserver;
}
@Override
public void visitOverallAggregate(String transactionType,
List<String> sharedQueryTexts, Aggregate overallAggregate) {
for (String sharedQueryText : sharedQueryTexts) {
Aggregate.SharedQueryText aggregateSharedQueryText = sharedQueryTextLimiter
.buildAggregateSharedQueryText(sharedQueryText);
String fullTextSha1 = aggregateSharedQueryText.getFullTextSha1();
if (!fullTextSha1.isEmpty()) {
fullTextSha1s.add(fullTextSha1);
}
requestObserver.onNext(AggregateStreamMessage.newBuilder()
.setSharedQueryText(aggregateSharedQueryText)
.build());
}
requestObserver.onNext(AggregateStreamMessage.newBuilder()
.setOverallAggregate(OverallAggregate.newBuilder()
.setTransactionType(transactionType)
.setAggregate(overallAggregate))
.build());
}
@Override
public void visitTransactionAggregate(String transactionType,
String transactionName, List<String> sharedQueryTexts,
Aggregate transactionAggregate) {
for (String sharedQueryText : sharedQueryTexts) {
requestObserver.onNext(AggregateStreamMessage.newBuilder()
.setSharedQueryText(sharedQueryTextLimiter
.buildAggregateSharedQueryText(sharedQueryText))
.build());
}
requestObserver.onNext(AggregateStreamMessage.newBuilder()
.setTransactionAggregate(TransactionAggregate.newBuilder()
.setTransactionType(transactionType)
.setTransactionName(transactionName)
.setAggregate(transactionAggregate))
.build());
}
}
private final AggregateReader aggregateReader;
private final List<String> fullTextSha1s = Lists.newArrayList();
private CollectAggregatesGrpcCall(AggregateReader aggregateReader) {
this.aggregateReader = aggregateReader;
}
@Override
public void call(StreamObserver<AggregateResponseMessage> responseObserver) {
final StreamObserver<AggregateStreamMessage> requestObserver =
collectorServiceStub.collectAggregateStream(responseObserver);
requestObserver.onNext(AggregateStreamMessage.newBuilder()
.setStreamHeader(AggregateStreamHeader.newBuilder()
.setAgentId(agentId)
.setCaptureTime(aggregateReader.captureTime()))
.build());
// need to clear in case this is a retry
fullTextSha1s.clear();
try {
aggregateReader.accept(new AggregateVisitorImpl(requestObserver));
} catch (Throwable t) {
logger.error(t.getMessage(), t);
requestObserver.onError(t);
return;
}
requestObserver.onCompleted();
}
@Override
public void doWithResponse(AggregateResponseMessage response) {
// Math.min is just for safety
nextAggregateDelayMillis = Math.min(response.getNextDelayMillis(), 30000);
for (String fullTextSha1 : fullTextSha1s) {
sharedQueryTextLimiter.onSuccessfullySentToCentralCollector(fullTextSha1);
}
}
}
private class CollectTraceGrpcCall extends GrpcCall<EmptyMessage> {
private final TraceReader traceReader;
private final List<String> fullTextSha1s = Lists.newArrayList();
private CollectTraceGrpcCall(TraceReader traceReader) {
this.traceReader = traceReader;
}
@Override
public void call(StreamObserver<EmptyMessage> responseObserver) {
StreamObserver<TraceStreamMessage> requestObserver =
collectorServiceStub.collectTraceStream(responseObserver);
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setStreamHeader(TraceStreamHeader.newBuilder()
.setAgentId(agentId)
.setTraceId(traceReader.traceId())
.setUpdate(traceReader.update()))
.build());
// need to clear in case this is a retry
fullTextSha1s.clear();
TraceVisitorImpl traceVisitor = new TraceVisitorImpl(requestObserver, fullTextSha1s);
try {
traceReader.accept(traceVisitor);
} catch (Throwable t) {
logger.error(t.getMessage(), t);
requestObserver.onError(t);
return;
}
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setStreamCounts(TraceStreamCounts.newBuilder()
.setSharedQueryTextCount(traceVisitor.sharedQueryTextCount)
.setEntryCount(traceVisitor.entryCount))
.build());
requestObserver.onCompleted();
}
@Override
public void doWithResponse(EmptyMessage response) {
for (String fullTextSha1 : fullTextSha1s) {
sharedQueryTextLimiter.onSuccessfullySentToCentralCollector(fullTextSha1);
}
}
}
private class TraceVisitorImpl implements TraceVisitor {
private final StreamObserver<TraceStreamMessage> requestObserver;
private final List<String> fullTextSha1s;
private final Map<String, Integer> sharedQueryTextIndexes = Maps.newHashMap();
private int sharedQueryTextCount;
private int entryCount;
private TraceVisitorImpl(StreamObserver<TraceStreamMessage> requestObserver,
List<String> fullTextSha1s) {
this.requestObserver = requestObserver;
this.fullTextSha1s = fullTextSha1s;
}
@Override
public int visitSharedQueryText(String sharedQueryText) {
Integer sharedQueryTextIndex = sharedQueryTextIndexes.get(sharedQueryText);
if (sharedQueryTextIndex != null) {
return sharedQueryTextIndex;
}
sharedQueryTextIndex = sharedQueryTextIndexes.size();
sharedQueryTextIndexes.put(sharedQueryText, sharedQueryTextIndex);
Trace.SharedQueryText traceSharedQueryText =
sharedQueryTextLimiter.buildTraceSharedQueryText(sharedQueryText);
String fullTextSha1 = traceSharedQueryText.getFullTextSha1();
if (!fullTextSha1.isEmpty()) {
fullTextSha1s.add(fullTextSha1);
}
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setSharedQueryText(traceSharedQueryText)
.build());
sharedQueryTextCount++;
return sharedQueryTextIndex;
}
@Override
public void visitEntry(Trace.Entry entry) {
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setEntry(entry)
.build());
entryCount++;
}
@Override
public void visitMainThreadProfile(Profile profile) {
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setMainThreadProfile(profile)
.build());
}
@Override
public void visitAuxThreadProfile(Profile profile) {
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setAuxThreadProfile(profile)
.build());
}
@Override
public void visitHeader(Trace.Header header) {
requestObserver.onNext(TraceStreamMessage.newBuilder()
.setHeader(header)
.build());
}
}
}