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

io.opentelemetry.contrib.awsxray.AwsXrayRemoteSampler Maven / Gradle / Ivy

/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.contrib.awsxray;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.contrib.awsxray.GetSamplingRulesResponse.SamplingRuleRecord;
import io.opentelemetry.contrib.awsxray.GetSamplingTargetsRequest.SamplingStatisticsDocument;
import io.opentelemetry.contrib.awsxray.GetSamplingTargetsResponse.SamplingTargetDocument;
import io.opentelemetry.sdk.common.Clock;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.data.LinkData;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.io.Closeable;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/** Remote sampler that gets sampling configuration from AWS X-Ray. */
public final class AwsXrayRemoteSampler implements Sampler, Closeable {

  static final long DEFAULT_TARGET_INTERVAL_NANOS = TimeUnit.SECONDS.toNanos(10);

  private static final Logger logger = Logger.getLogger(AwsXrayRemoteSampler.class.getName());

  private final Resource resource;
  private final Clock clock;
  private final Sampler initialSampler;
  private final XraySamplerClient client;
  private final ScheduledExecutorService executor;
  // Unique per-sampler client ID, generated as a random string.
  private final String clientId;
  private final long pollingIntervalNanos;
  private final Iterator jitterNanos;

  @Nullable private volatile ScheduledFuture pollFuture;
  @Nullable private volatile ScheduledFuture fetchTargetsFuture;
  @Nullable private volatile GetSamplingRulesResponse previousRulesResponse;
  private volatile Sampler sampler;

  /**
   * Returns a {@link AwsXrayRemoteSamplerBuilder} with the given {@link Resource}. This {@link
   * Resource} should be the same as what the OpenTelemetry SDK is configured with.
   */
  // TODO(anuraaga): Deprecate after
  // https://github.com/open-telemetry/opentelemetry-specification/issues/1588
  public static AwsXrayRemoteSamplerBuilder newBuilder(Resource resource) {
    return new AwsXrayRemoteSamplerBuilder(resource);
  }

  AwsXrayRemoteSampler(
      Resource resource,
      Clock clock,
      String endpoint,
      Sampler initialSampler,
      long pollingIntervalNanos) {
    this.resource = resource;
    this.clock = clock;
    this.initialSampler = initialSampler;
    client = new XraySamplerClient(endpoint);
    executor =
        Executors.newSingleThreadScheduledExecutor(
            runnable -> {
              Thread t = Executors.defaultThreadFactory().newThread(runnable);
              try {
                t.setDaemon(true);
                t.setName("xray-rules-poller");
              } catch (SecurityException e) {
                // Well, we tried.
              }
              return t;
            });

    clientId = generateClientId();

    sampler = initialSampler;

    this.pollingIntervalNanos = pollingIntervalNanos;
    // Add ~1% of jitter
    jitterNanos = ThreadLocalRandom.current().longs(0, pollingIntervalNanos / 100).iterator();

    // Execute first update right away on the executor thread.
    executor.execute(this::getAndUpdateSampler);
  }

  @Override
  public SamplingResult shouldSample(
      Context parentContext,
      String traceId,
      String name,
      SpanKind spanKind,
      Attributes attributes,
      List parentLinks) {
    return sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
  }

  @Override
  public String getDescription() {
    return "AwsXrayRemoteSampler{" + sampler.getDescription() + "}";
  }

  private void getAndUpdateSampler() {
    try {
      // No pagination support yet, or possibly ever.
      GetSamplingRulesResponse response =
          client.getSamplingRules(GetSamplingRulesRequest.create(null));
      if (!response.equals(previousRulesResponse)) {
        sampler =
            new XrayRulesSampler(
                clientId,
                resource,
                clock,
                initialSampler,
                response.getSamplingRules().stream()
                    .map(SamplingRuleRecord::getRule)
                    .collect(Collectors.toList()));
        previousRulesResponse = response;
        ScheduledFuture existingFetchTargetsFuture = fetchTargetsFuture;
        if (existingFetchTargetsFuture != null) {
          existingFetchTargetsFuture.cancel(false);
        }
        fetchTargetsFuture =
            executor.schedule(
                this::fetchTargets, DEFAULT_TARGET_INTERVAL_NANOS, TimeUnit.NANOSECONDS);
      }
    } catch (Throwable t) {
      logger.log(Level.FINE, "Failed to update sampler", t);
    }
    scheduleSamplerUpdate();
  }

  private void scheduleSamplerUpdate() {
    long delay = pollingIntervalNanos + jitterNanos.next();
    pollFuture = executor.schedule(this::getAndUpdateSampler, delay, TimeUnit.NANOSECONDS);
  }

  /**
   * returns the duration until the next scheduled sampler update or null if no next update is
   * scheduled yet.
   *
   * 

only used for testing. */ @Nullable Duration getNextSamplerUpdateScheduledDuration() { ScheduledFuture pollFuture = this.pollFuture; if (pollFuture == null) { return null; } return Duration.ofNanos(pollFuture.getDelay(TimeUnit.NANOSECONDS)); } private void fetchTargets() { if (!(sampler instanceof XrayRulesSampler)) { throw new IllegalStateException("Programming bug."); } XrayRulesSampler xrayRulesSampler = (XrayRulesSampler) sampler; try { Date now = Date.from(Instant.ofEpochSecond(0, clock.now())); List statistics = xrayRulesSampler.snapshot(now); Set requestedTargetRuleNames = statistics.stream() .map(SamplingStatisticsDocument::getRuleName) .collect(Collectors.toSet()); GetSamplingTargetsResponse response = client.getSamplingTargets(GetSamplingTargetsRequest.create(statistics)); Map targets = response.getDocuments().stream() .collect(Collectors.toMap(SamplingTargetDocument::getRuleName, Function.identity())); sampler = xrayRulesSampler = xrayRulesSampler.withTargets(targets, requestedTargetRuleNames, now); } catch (Throwable t) { // Might be a transient API failure, try again after a default interval. fetchTargetsFuture = executor.schedule( this::fetchTargets, DEFAULT_TARGET_INTERVAL_NANOS, TimeUnit.NANOSECONDS); return; } long nextTargetFetchIntervalNanos = xrayRulesSampler.nextTargetFetchTimeNanos() - clock.nanoTime(); fetchTargetsFuture = executor.schedule(this::fetchTargets, nextTargetFetchIntervalNanos, TimeUnit.NANOSECONDS); } @Override @SuppressWarnings("Interruption") public void close() { ScheduledFuture pollFuture = this.pollFuture; if (pollFuture != null) { pollFuture.cancel(true); } executor.shutdownNow(); // No flushing behavior so no need to wait for the shutdown. } private static String generateClientId() { Random rand = new Random(); char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; char[] clientIdChars = new char[24]; for (int i = 0; i < clientIdChars.length; i++) { clientIdChars[i] = hex[rand.nextInt(hex.length)]; } return new String(clientIdChars); } // Visible for testing XraySamplerClient getClient() { return client; } // Visible for testing Resource getResource() { return resource; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy