nl.topicus.jdbc.shaded.io.grpc.internal.CensusStatsModule Maven / Gradle / Ivy
Show all versions of spanner-jdbc Show documentation
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package nl.topicus.jdbc.shaded.io.grpc.internal;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.MoreObjects.firstNonNull;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions.checkNotNull;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions.checkState;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.annotations.VisibleForTesting;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Stopwatch;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Supplier;
import nl.topicus.jdbc.shaded.com.google.instrumentation.stats.MeasurementMap;
import nl.topicus.jdbc.shaded.com.google.instrumentation.stats.RpcConstants;
import nl.topicus.jdbc.shaded.com.google.instrumentation.stats.StatsContext;
import nl.topicus.jdbc.shaded.com.google.instrumentation.stats.StatsContextFactory;
import nl.topicus.jdbc.shaded.com.google.instrumentation.stats.TagValue;
import nl.topicus.jdbc.shaded.io.grpc.CallOptions;
import nl.topicus.jdbc.shaded.io.grpc.Channel;
import nl.topicus.jdbc.shaded.io.grpc.ClientCall;
import nl.topicus.jdbc.shaded.io.grpc.ClientInterceptor;
import nl.topicus.jdbc.shaded.io.grpc.ClientStreamTracer;
import nl.topicus.jdbc.shaded.io.grpc.Context;
import nl.topicus.jdbc.shaded.io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import nl.topicus.jdbc.shaded.io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
import nl.topicus.jdbc.shaded.io.grpc.Metadata;
import nl.topicus.jdbc.shaded.io.grpc.MethodDescriptor;
import nl.topicus.jdbc.shaded.io.grpc.ServerStreamTracer;
import nl.topicus.jdbc.shaded.io.grpc.Status;
import nl.topicus.jdbc.shaded.io.grpc.StreamTracer;
import java.nl.topicus.jdbc.shaded.io.ByteArrayInputStream;
import java.nl.topicus.jdbc.shaded.io.ByteArrayOutputStream;
import java.nl.topicus.jdbc.shaded.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import nl.topicus.jdbc.shaded.javax.annotation.Nullable;
/**
* Provides factories for {@link StreamTracer} that records stats to Census.
*
* On the client-side, a factory is created for each call, because ClientCall starts earlier than
* the ClientStream, and in some cases may even not create a ClientStream at all. Therefore, it's
* the factory that reports the summary to Census.
*
*
On the server-side, there is only one ServerStream per each ServerCall, and ServerStream
* starts earlier than the ServerCall. Therefore, only one tracer is created per stream/call and
* it's the tracer that reports the summary to Census.
*/
final class CensusStatsModule {
private static final Logger logger = Logger.getLogger(CensusStatsModule.class.getName());
private static final double NANOS_PER_MILLI = TimeUnit.MILLISECONDS.toNanos(1);
private static final ClientTracer BLANK_CLIENT_TRACER = new ClientTracer();
// TODO(zhangkun): point to Census's StatsContext key once they've made it public
@VisibleForTesting
static final Context.Key STATS_CONTEXT_KEY =
Context.key("nl.topicus.jdbc.shaded.io.grpc.internal.StatsContext");
private final StatsContextFactory statsCtxFactory;
private final Supplier stopwatchSupplier;
@VisibleForTesting
final Metadata.Key statsHeader;
private final StatsClientInterceptor clientInterceptor = new StatsClientInterceptor();
private final ServerTracerFactory serverTracerFactory = new ServerTracerFactory();
private final boolean propagateTags;
CensusStatsModule(
final StatsContextFactory statsCtxFactory, Supplier stopwatchSupplier,
boolean propagateTags) {
this.statsCtxFactory = checkNotNull(statsCtxFactory, "statsCtxFactory");
this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier");
this.propagateTags = propagateTags;
this.statsHeader =
Metadata.Key.of("grpc-tags-bin", new Metadata.BinaryMarshaller() {
@Override
public byte[] toBytes(StatsContext context) {
// TODO(carl-mastrangelo): currently we only make sure the correctness. We may need to
// optimize out the allocation and copy in the future.
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
context.serialize(buffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
return buffer.toByteArray();
}
@Override
public StatsContext parseBytes(byte[] serialized) {
try {
return statsCtxFactory.deserialize(new ByteArrayInputStream(serialized));
} catch (Exception e) {
logger.log(Level.FINE, "Failed to parse stats header", e);
return statsCtxFactory.getDefault();
}
}
});
}
/**
* Creates a {@link ClientCallTracer} for a new call.
*/
@VisibleForTesting
ClientCallTracer newClientCallTracer(StatsContext parentCtx, String fullMethodName) {
return new ClientCallTracer(parentCtx, fullMethodName);
}
/**
* Returns the server tracer factory.
*/
ServerStreamTracer.Factory getServerTracerFactory() {
return serverTracerFactory;
}
/**
* Returns the client interceptor that facilitates Census-based stats reporting.
*/
ClientInterceptor getClientInterceptor() {
return clientInterceptor;
}
private static final class ClientTracer extends ClientStreamTracer {
final AtomicLong outboundWireSize = new AtomicLong();
final AtomicLong inboundWireSize = new AtomicLong();
final AtomicLong outboundUncompressedSize = new AtomicLong();
final AtomicLong inboundUncompressedSize = new AtomicLong();
@Override
public void outboundWireSize(long bytes) {
outboundWireSize.addAndGet(bytes);
}
@Override
public void inboundWireSize(long bytes) {
inboundWireSize.addAndGet(bytes);
}
@Override
public void outboundUncompressedSize(long bytes) {
outboundUncompressedSize.addAndGet(bytes);
}
@Override
public void inboundUncompressedSize(long bytes) {
inboundUncompressedSize.addAndGet(bytes);
}
}
@VisibleForTesting
final class ClientCallTracer extends ClientStreamTracer.Factory {
private final String fullMethodName;
private final Stopwatch stopwatch;
private final AtomicReference streamTracer = new AtomicReference();
private final AtomicBoolean callEnded = new AtomicBoolean(false);
private final StatsContext parentCtx;
ClientCallTracer(StatsContext parentCtx, String fullMethodName) {
this.parentCtx = checkNotNull(parentCtx, "parentCtx");
this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
this.stopwatch = stopwatchSupplier.get().start();
}
@Override
public ClientStreamTracer newClientStreamTracer(Metadata headers) {
ClientTracer tracer = new ClientTracer();
// TODO(zhangkun83): Once retry or hedging is implemented, a ClientCall may start more than
// one streams. We will need to update this file to support them.
checkState(streamTracer.nl.topicus.jdbc.shaded.com.areAndSet(null, tracer),
"Are you creating multiple streams per call? This class doesn't yet support this case.");
if (propagateTags) {
headers.discardAll(statsHeader);
if (parentCtx != statsCtxFactory.getDefault()) {
headers.put(statsHeader, parentCtx);
}
}
return tracer;
}
/**
* Record a finished call and mark the current time as the end time.
*
* Can be called from any thread without synchronization. Calling it the second time or more
* is a no-op.
*/
void callEnded(Status status) {
if (!callEnded.nl.topicus.jdbc.shaded.com.areAndSet(false, true)) {
return;
}
stopwatch.stop();
long roundtripNanos = stopwatch.elapsed(TimeUnit.NANOSECONDS);
ClientTracer tracer = streamTracer.get();
if (tracer == null) {
tracer = BLANK_CLIENT_TRACER;
}
MeasurementMap.Builder builder = MeasurementMap.builder()
// The metrics are in double
.put(RpcConstants.RPC_CLIENT_ROUNDTRIP_LATENCY, roundtripNanos / NANOS_PER_MILLI)
.put(RpcConstants.RPC_CLIENT_REQUEST_BYTES, tracer.outboundWireSize.get())
.put(RpcConstants.RPC_CLIENT_RESPONSE_BYTES, tracer.inboundWireSize.get())
.put(
RpcConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
tracer.outboundUncompressedSize.get())
.put(
RpcConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
tracer.inboundUncompressedSize.get());
if (!status.isOk()) {
builder.put(RpcConstants.RPC_CLIENT_ERROR_COUNT, 1.0);
}
parentCtx
.with(
RpcConstants.RPC_CLIENT_METHOD, TagValue.create(fullMethodName),
RpcConstants.RPC_STATUS, TagValue.create(status.getCode().toString()))
.record(builder.build());
}
}
private final class ServerTracer extends ServerStreamTracer {
private final String fullMethodName;
@Nullable
private final StatsContext parentCtx;
private final AtomicBoolean streamClosed = new AtomicBoolean(false);
private final Stopwatch stopwatch;
private final AtomicLong outboundWireSize = new AtomicLong();
private final AtomicLong inboundWireSize = new AtomicLong();
private final AtomicLong outboundUncompressedSize = new AtomicLong();
private final AtomicLong inboundUncompressedSize = new AtomicLong();
ServerTracer(String fullMethodName, StatsContext parentCtx) {
this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
this.parentCtx = checkNotNull(parentCtx, "parentCtx");
this.stopwatch = stopwatchSupplier.get().start();
}
@Override
public void outboundWireSize(long bytes) {
outboundWireSize.addAndGet(bytes);
}
@Override
public void inboundWireSize(long bytes) {
inboundWireSize.addAndGet(bytes);
}
@Override
public void outboundUncompressedSize(long bytes) {
outboundUncompressedSize.addAndGet(bytes);
}
@Override
public void inboundUncompressedSize(long bytes) {
inboundUncompressedSize.addAndGet(bytes);
}
/**
* Record a finished stream and mark the current time as the end time.
*
*
Can be called from any thread without synchronization. Calling it the second time or more
* is a no-op.
*/
@Override
public void streamClosed(Status status) {
if (!streamClosed.nl.topicus.jdbc.shaded.com.areAndSet(false, true)) {
return;
}
stopwatch.stop();
long elapsedTimeNanos = stopwatch.elapsed(TimeUnit.NANOSECONDS);
MeasurementMap.Builder builder = MeasurementMap.builder()
// The metrics are in double
.put(RpcConstants.RPC_SERVER_SERVER_LATENCY, elapsedTimeNanos / NANOS_PER_MILLI)
.put(RpcConstants.RPC_SERVER_RESPONSE_BYTES, outboundWireSize.get())
.put(RpcConstants.RPC_SERVER_REQUEST_BYTES, inboundWireSize.get())
.put(
RpcConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
outboundUncompressedSize.get())
.put(
RpcConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
inboundUncompressedSize.get());
if (!status.isOk()) {
builder.put(RpcConstants.RPC_SERVER_ERROR_COUNT, 1.0);
}
StatsContext ctx = firstNonNull(parentCtx, statsCtxFactory.getDefault());
ctx
.with(
RpcConstants.RPC_SERVER_METHOD, TagValue.create(fullMethodName),
RpcConstants.RPC_STATUS, TagValue.create(status.getCode().toString()))
.record(builder.build());
}
@Override
public Context filterContext(Context context) {
if (parentCtx != statsCtxFactory.getDefault()) {
return context.withValue(STATS_CONTEXT_KEY, parentCtx);
}
return context;
}
}
private final class ServerTracerFactory extends ServerStreamTracer.Factory {
@Override
public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) {
StatsContext parentCtx = headers.get(statsHeader);
if (parentCtx == null) {
parentCtx = statsCtxFactory.getDefault();
}
return new ServerTracer(fullMethodName, parentCtx);
}
}
private class StatsClientInterceptor implements ClientInterceptor {
@Override
public ClientCall interceptCall(
MethodDescriptor method, CallOptions callOptions, Channel next) {
// New RPCs on client-side inherit the stats context from the current Context.
StatsContext parentCtx = STATS_CONTEXT_KEY.get();
if (parentCtx == null) {
parentCtx = statsCtxFactory.getDefault();
}
final ClientCallTracer tracerFactory =
newClientCallTracer(parentCtx, method.getFullMethodName());
ClientCall call =
next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory));
return new SimpleForwardingClientCall(call) {
@Override
public void start(Listener responseListener, Metadata headers) {
delegate().start(
new SimpleForwardingClientCallListener(responseListener) {
@Override
public void onClose(Status status, Metadata trailers) {
tracerFactory.callEnded(status);
super.onClose(status, trailers);
}
},
headers);
}
};
}
}
}