All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.grpc.opentelemetry.OpenTelemetryTracingModule Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 The gRPC 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 io.grpc.opentelemetry;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED;

import com.google.common.annotations.VisibleForTesting;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientStreamTracer;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.ServerStreamTracer;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
 * Provides factories for {@link io.grpc.StreamTracer} that records tracing to OpenTelemetry.
 */
final class OpenTelemetryTracingModule {
  private static final Logger logger = Logger.getLogger(OpenTelemetryTracingModule.class.getName());

  @VisibleForTesting
  static final String OTEL_TRACING_SCOPE_NAME = "grpc-java";
  @Nullable
  private static final AtomicIntegerFieldUpdater callEndedUpdater;
  @Nullable
  private static final AtomicIntegerFieldUpdater streamClosedUpdater;

  /*
   * When using Atomic*FieldUpdater, some Samsung Android 5.0.x devices encounter a bug in their JDK
   * reflection API that triggers a NoSuchFieldException. When this occurs, we fallback to
   * (potentially racy) direct updates of the volatile variables.
   */
  static {
    AtomicIntegerFieldUpdater tmpCallEndedUpdater;
    AtomicIntegerFieldUpdater tmpStreamClosedUpdater;
    try {
      tmpCallEndedUpdater =
          AtomicIntegerFieldUpdater.newUpdater(CallAttemptsTracerFactory.class, "callEnded");
      tmpStreamClosedUpdater =
          AtomicIntegerFieldUpdater.newUpdater(ServerTracer.class, "streamClosed");
    } catch (Throwable t) {
      logger.log(Level.SEVERE, "Creating atomic field updaters failed", t);
      tmpCallEndedUpdater = null;
      tmpStreamClosedUpdater = null;
    }
    callEndedUpdater = tmpCallEndedUpdater;
    streamClosedUpdater = tmpStreamClosedUpdater;
  }

  private final Tracer otelTracer;
  private final ContextPropagators contextPropagators;
  private final MetadataGetter metadataGetter = MetadataGetter.getInstance();
  private final MetadataSetter metadataSetter = MetadataSetter.getInstance();
  private final TracingClientInterceptor clientInterceptor = new TracingClientInterceptor();
  private final ServerTracerFactory serverTracerFactory = new ServerTracerFactory();

  OpenTelemetryTracingModule(OpenTelemetry openTelemetry) {
    this.otelTracer = checkNotNull(openTelemetry.getTracer(OTEL_TRACING_SCOPE_NAME), "otelTracer");
    this.contextPropagators = checkNotNull(openTelemetry.getPropagators(), "contextPropagators");
  }

  /**
   * Creates a {@link CallAttemptsTracerFactory} for a new call.
   */
  @VisibleForTesting
  CallAttemptsTracerFactory newClientCallTracer(Span clientSpan, MethodDescriptor method) {
    return new CallAttemptsTracerFactory(clientSpan, method);
  }

  /**
   * Returns the server tracer factory.
   */
  ServerStreamTracer.Factory getServerTracerFactory() {
    return serverTracerFactory;
  }

  /**
   * Returns the client interceptor that facilitates otel tracing reporting.
   */
  ClientInterceptor getClientInterceptor() {
    return clientInterceptor;
  }

  @VisibleForTesting
  final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory {
    volatile int callEnded;
    private final Span clientSpan;
    private final String fullMethodName;

    CallAttemptsTracerFactory(Span clientSpan, MethodDescriptor method) {
      checkNotNull(method, "method");
      this.fullMethodName = checkNotNull(method.getFullMethodName(), "fullMethodName");
      this.clientSpan = checkNotNull(clientSpan, "clientSpan");
    }

    @Override
    public ClientStreamTracer newClientStreamTracer(
        ClientStreamTracer.StreamInfo info, Metadata headers) {
      Span attemptSpan = otelTracer.spanBuilder(
              "Attempt." + fullMethodName.replace('/', '.'))
          .setParent(Context.current().with(clientSpan))
          .startSpan();
      attemptSpan.setAttribute(
          "previous-rpc-attempts", info.getPreviousAttempts());
      attemptSpan.setAttribute(
          "transparent-retry",info.isTransparentRetry());
      if (info.getCallOptions().getOption(NAME_RESOLUTION_DELAYED) != null) {
        clientSpan.addEvent("Delayed name resolution complete");
      }
      return new ClientTracer(attemptSpan, clientSpan);
    }

    /**
     * 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(io.grpc.Status status) { if (callEndedUpdater != null) { if (callEndedUpdater.getAndSet(this, 1) != 0) { return; } } else { if (callEnded != 0) { return; } callEnded = 1; } endSpanWithStatus(clientSpan, status); } } private final class ClientTracer extends ClientStreamTracer { private final Span span; private final Span parentSpan; volatile int seqNo; boolean isPendingStream; ClientTracer(Span span, Span parentSpan) { this.span = checkNotNull(span, "span"); this.parentSpan = checkNotNull(parentSpan, "parent span"); } @Override public void streamCreated(Attributes transportAtts, Metadata headers) { contextPropagators.getTextMapPropagator().inject(Context.current().with(span), headers, metadataSetter); if (isPendingStream) { span.addEvent("Delayed LB pick complete"); } } @Override public void createPendingStream() { isPendingStream = true; } @Override public void outboundMessageSent( int seqNo, long optionalWireSize, long optionalUncompressedSize) { recordOutboundMessageSentEvent(span, seqNo, optionalWireSize, optionalUncompressedSize); } @Override public void inboundMessageRead( int seqNo, long optionalWireSize, long optionalUncompressedSize) { //TODO(yifeizhuang): needs support from message deframer. if (optionalWireSize != optionalUncompressedSize) { recordInboundCompressedMessage(span, seqNo, optionalWireSize); } } @Override public void inboundMessage(int seqNo) { this.seqNo = seqNo; } @Override public void inboundUncompressedSize(long bytes) { recordInboundMessageSize(parentSpan, seqNo, bytes); } @Override public void streamClosed(io.grpc.Status status) { endSpanWithStatus(span, status); } } private final class ServerTracer extends ServerStreamTracer { private final Span span; volatile int streamClosed; private int seqNo; ServerTracer(String fullMethodName, @Nullable Span remoteSpan) { checkNotNull(fullMethodName, "fullMethodName"); this.span = otelTracer.spanBuilder(generateTraceSpanName(true, fullMethodName)) .setParent(remoteSpan == null ? null : Context.current().with(remoteSpan)) .startSpan(); } /** * 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(io.grpc.Status status) { if (streamClosedUpdater != null) { if (streamClosedUpdater.getAndSet(this, 1) != 0) { return; } } else { if (streamClosed != 0) { return; } streamClosed = 1; } endSpanWithStatus(span, status); } @Override public void outboundMessageSent( int seqNo, long optionalWireSize, long optionalUncompressedSize) { recordOutboundMessageSentEvent(span, seqNo, optionalWireSize, optionalUncompressedSize); } @Override public void inboundMessageRead( int seqNo, long optionalWireSize, long optionalUncompressedSize) { if (optionalWireSize != optionalUncompressedSize) { recordInboundCompressedMessage(span, seqNo, optionalWireSize); } } @Override public void inboundMessage(int seqNo) { this.seqNo = seqNo; } @Override public void inboundUncompressedSize(long bytes) { recordInboundMessageSize(span, seqNo, bytes); } } @VisibleForTesting final class ServerTracerFactory extends ServerStreamTracer.Factory { @SuppressWarnings("ReferenceEquality") @Override public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) { Context context = contextPropagators.getTextMapPropagator().extract( Context.current(), headers, metadataGetter ); Span remoteSpan = Span.fromContext(context); if (remoteSpan == Span.getInvalid()) { remoteSpan = null; } return new ServerTracer(fullMethodName, remoteSpan); } } @VisibleForTesting final class TracingClientInterceptor implements ClientInterceptor { @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { Span clientSpan = otelTracer.spanBuilder( generateTraceSpanName(false, method.getFullMethodName())) .startSpan(); final CallAttemptsTracerFactory tracerFactory = newClientCallTracer(clientSpan, method); 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(io.grpc.Status status, Metadata trailers) { tracerFactory.callEnded(status); super.onClose(status, trailers); } }, headers); } }; } } // Attribute named "message-size" always means the message size the application sees. // If there was compression, additional event reports "message-size-compressed". // // An example trace with message compression: // // Sending: // |-- Event 'Outbound message sent', attributes('sequence-numer' = 0, 'message-size' = 7854, // 'message-size-compressed' = 5493) ----| // // Receiving: // |-- Event 'Inbound compressed message', attributes('sequence-numer' = 0, // 'message-size-compressed' = 5493 ) ----| // |-- Event 'Inbound message received', attributes('sequence-numer' = 0, // 'message-size' = 7854) ----| // // An example trace with no message compression: // // Sending: // |-- Event 'Outbound message sent', attributes('sequence-numer' = 0, 'message-size' = 7854) ---| // // Receiving: // |-- Event 'Inbound message received', attributes('sequence-numer' = 0, // 'message-size' = 7854) ----| private void recordOutboundMessageSentEvent(Span span, int seqNo, long optionalWireSize, long optionalUncompressedSize) { AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder(); attributesBuilder.put("sequence-number", seqNo); if (optionalUncompressedSize != -1) { attributesBuilder.put("message-size", optionalUncompressedSize); } if (optionalWireSize != -1 && optionalWireSize != optionalUncompressedSize) { attributesBuilder.put("message-size-compressed", optionalWireSize); } span.addEvent("Outbound message sent", attributesBuilder.build()); } private void recordInboundCompressedMessage(Span span, int seqNo, long optionalWireSize) { AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder(); attributesBuilder.put("sequence-number", seqNo); attributesBuilder.put("message-size-compressed", optionalWireSize); span.addEvent("Inbound compressed message", attributesBuilder.build()); } private void recordInboundMessageSize(Span span, int seqNo, long bytes) { AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder(); attributesBuilder.put("sequence-number", seqNo); attributesBuilder.put("message-size", bytes); span.addEvent("Inbound message received", attributesBuilder.build()); } private String generateErrorStatusDescription(io.grpc.Status status) { if (status.getDescription() != null) { return status.getCode() + ": " + status.getDescription(); } else { return status.getCode().toString(); } } private void endSpanWithStatus(Span span, io.grpc.Status status) { if (status.isOk()) { span.setStatus(StatusCode.OK); } else { span.setStatus(StatusCode.ERROR, generateErrorStatusDescription(status)); } span.end(); } /** * Convert a full method name to a tracing span name. * * @param isServer {@code false} if the span is on the client-side, {@code true} if on the * server-side * @param fullMethodName the method name as returned by * {@link MethodDescriptor#getFullMethodName}. */ @VisibleForTesting static String generateTraceSpanName(boolean isServer, String fullMethodName) { String prefix = isServer ? "Recv" : "Sent"; return prefix + "." + fullMethodName.replace('/', '.'); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy