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

com.wavefront.sdk.common.Utils Maven / Gradle / Ivy

package com.wavefront.sdk.common;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wavefront.sdk.common.annotation.NonNull;
import com.wavefront.sdk.common.annotation.Nullable;
import com.wavefront.sdk.entities.events.EventDTO;
import com.wavefront.sdk.entities.histograms.HistogramGranularity;
import com.wavefront.sdk.entities.tracing.SpanLog;
import com.wavefront.sdk.entities.tracing.SpanLogsDTO;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;

import static com.wavefront.sdk.common.Constants.RESOURCES_ROOT;
import static com.wavefront.sdk.common.Constants.SEMVER_PATTERN;
import static com.wavefront.sdk.common.Constants.SPAN_LOG_KEY;
import static com.wavefront.sdk.common.Constants.VERSION;

/**
 * Common Util methods
 *
 * @author Sushant Dewan ([email protected]).
 */
public class Utils {

  private static final Logger logger = Logger.getLogger(
          Utils.class.getCanonicalName());

  private static final ObjectMapper JSON_PARSER = new ObjectMapper();

  public static String sanitize(String s) {
    return sanitizeInternal(s, true, false);
  }

  public static String sanitize(String s, boolean ignoreSlash) {
    return sanitizeInternal(s, true, ignoreSlash);
  }

  public static String sanitizeWithoutQuotes(String s) {
    return sanitizeInternal(s, false, false);
  }

  public static String sanitizeValue(String s) {
    /*
     * Sanitize string of tags value, etc.
     */
    String res = s.trim();
    if (s.contains("\"") || s.contains("'")) {
      // for single quotes, once we are double-quoted, single quotes can exist happily inside it.
      res = res.replace("\"", "\\\"");
    }
    return "\"" + res.replace("\n", "\\n") + "\"";
  }

  public static String metricToLineData(String name, double value, @Nullable Long timestamp,
                                        String source, @Nullable Map tags,
                                        String defaultSource) {
    /*
     * Wavefront Metrics Data format
     *   [] source= [pointTags]
     *
     * Example: "new-york.power.usage 42422 1533531013 source=localhost datacenter=dc1"
     */

    if (source == null || source.isEmpty()) {
      source = defaultSource;
    }
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("metrics name cannot be blank " +
          getContextInfo(name, source, tags));
    }
    if (source == null || source.isEmpty()) {
      throw new IllegalArgumentException("source cannot be blank " +
          getContextInfo(name, source, tags));
    }

    final StringBuilder sb = new StringBuilder();
    sb.append(sanitize(name));
    sb.append(' ');
    sb.append(value);
    if (timestamp != null) {
      sb.append(' ');
      sb.append(timestamp);
    }
    sb.append(" source=");
    sb.append(sanitizeValue(source));
    if (tags != null) {
      for (final Map.Entry tag : tags.entrySet()) {
        String key = tag.getKey();
        String val = tag.getValue();
        if (key == null || key.isEmpty()) {
          throw new IllegalArgumentException("metric point tag key cannot be blank " +
              getContextInfo(name, source, tags));
        }
        if (val == null || val.isEmpty()) {
          throw new IllegalArgumentException("metric point tag value cannot be blank for " +
              "tag key: " + key + " " + getContextInfo(name, source, tags));
        }
        sb.append(' ');
        sb.append(sanitize(key));
        sb.append('=');
        sb.append(sanitizeValue(val));
      }
    }
    sb.append('\n');
    return sb.toString();
  }

  public static String logToLineData(String name, double value, @Nullable Long timestamp,
                                     String source, @Nullable Map tags,
                                     String defaultSource) {
    if (source == null || source.isEmpty()) {
      source = defaultSource;
    }
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("log name cannot be blank " +
              getContextInfo(name, source, tags));
    }
    if (source == null || source.isEmpty()) {
      throw new IllegalArgumentException("source cannot be blank " +
              getContextInfo(name, source, tags));
    }

    final StringBuilder sb = new StringBuilder();
    sb.append(sanitize(name, true));
    sb.append(' ');
    sb.append(value);
    if (timestamp != null) {
      sb.append(' ');
      sb.append(timestamp);
    }
    sb.append(" source=");
    sb.append(sanitizeValue(source));
    if (tags != null) {
      for (final Map.Entry tag : tags.entrySet()) {
        String key = tag.getKey();
        String val = tag.getValue();
        if (key == null || key.isEmpty()) {
          throw new IllegalArgumentException("log label key cannot be blank " +
                  getContextInfo(name, source, tags));
        }
        if (val == null || val.isEmpty()) {
          throw new IllegalArgumentException("log label value cannot be blank for " +
                  "log label key: " + key + " " + getContextInfo(name, source, tags));
        }
        sb.append(' ');
        sb.append(sanitize(key));
        sb.append('=');
        sb.append(sanitizeValue(val));
      }
    }
    sb.append('\n');
    return sb.toString();
  }

  public static String histogramToLineData(String name, List> centroids,
                                           Set histogramGranularities,
                                           @Nullable Long timestamp, String source,
                                           @Nullable Map tags,
                                           String defaultSource) {
    /*
     * Wavefront Histogram Data format
     * {!M | !H | !D} [] #  [centroids]  source=
     *   [pointTags]
     *
     * Example: "!M 1533531013 #20 30.0 #10 5.1 request.latency source=appServer1 region=us-west"
     */

    if (source == null || source.isEmpty()) {
      source = defaultSource;
    }
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("histogram name cannot be blank " +
          getContextInfo(name, source, tags));
    }
    if (source == null || source.isEmpty()) {
      throw new IllegalArgumentException("histogram source cannot be blank " +
          getContextInfo(name, source, tags));
    }
    if (histogramGranularities == null || histogramGranularities.isEmpty()) {
      throw new IllegalArgumentException("Histogram granularities cannot be null or empty " +
          getContextInfo(name, source, tags));
    }
    if (centroids == null || centroids.isEmpty()) {
      throw new IllegalArgumentException("A distribution should have at least one centroid " +
          getContextInfo(name, source, tags));
    }
    final StringBuilder sb = new StringBuilder();
    for (HistogramGranularity histogramGranularity : histogramGranularities) {
      sb.append(histogramGranularity.identifier);
      if (timestamp != null) {
        sb.append(' ');
        sb.append(Long.toString(timestamp));
      }
      sb.append(' ');
      appendCompactedCentroids(sb, centroids);
      sb.append(sanitize(name));
      sb.append(" source=");
      sb.append(sanitizeValue(source));
      if (tags != null) {
        for (final Map.Entry tag : tags.entrySet()) {
          String key = tag.getKey();
          String val = tag.getValue();
          if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("histogram tag key cannot be blank " +
                getContextInfo(name, source, tags));
          }
          if (val == null || val.isEmpty()) {
            throw new IllegalArgumentException("histogram tag value cannot be blank for " +
                "tag key: " + key + " " + getContextInfo(name, source, tags));
          }
          sb.append(' ');
          sb.append(sanitize(tag.getKey()));
          sb.append('=');
          sb.append(sanitizeValue(tag.getValue()));
        }
      }
      sb.append('\n');
    }
    return sb.toString();
  }

  public static String tracingSpanToLineData(String name, long startMillis, long durationMillis,
                                             String source, UUID traceId, UUID spanId,
                                             @Nullable List parents,
                                             @Nullable List followsFrom,
                                             @Nullable List> tags,
                                             @Nullable List spanLogs, String defaultSource) {
    /*
     * Wavefront Tracing Span Data format
     *  source= [pointTags]  
     *
     * Example: "getAllUsers source=localhost
     *           traceId=7b3bf470-9456-11e8-9eb6-529269fb1459
     *           spanId=0313bafe-9457-11e8-9eb6-529269fb1459
     *           parent=2f64e538-9457-11e8-9eb6-529269fb1459
     *           application=Wavefront http.method=GET
     *           1533531013 343500"
     */

    if (source == null || source.isEmpty()) {
      source = defaultSource;
    }
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("span name cannot be blank " +
          getContextInfo(name, source, tags));
    }
    if (source == null || source.isEmpty()) {
      throw new IllegalArgumentException("span source cannot be blank " +
          getContextInfo(name, source, tags));
    }
    final StringBuilder sb = new StringBuilder();
    sb.append(sanitizeValue(name));
    sb.append(" source=");
    sb.append(sanitizeValue(source));
    sb.append(" traceId=");
    sb.append(traceId);
    sb.append(" spanId=");
    sb.append(spanId);
    if (parents != null) {
      for (UUID parent : parents) {
        sb.append(" parent=");
        sb.append(parent.toString());
      }
    }
    if (followsFrom != null) {
      for (UUID item : followsFrom) {
        sb.append(" followsFrom=");
        sb.append(item.toString());
      }
    }
    if (tags != null) {
      for (final Pair tag : tags) {
        String key = tag._1;
        String val = tag._2;
        if (key == null || key.isEmpty()) {
          throw new IllegalArgumentException("span tag key cannot be blank " +
              getContextInfo(name, source, tags));
        }
        if (val == null || val.isEmpty()) {
          throw new IllegalArgumentException("span tag value cannot be blank for " +
              "tag key: " + key + " " + getContextInfo(name, source, tags));
        }
        sb.append(' ');
        sb.append(sanitize(key));
        sb.append('=');
        sb.append(sanitizeValue(val));
      }
    }
    if (spanLogs != null && !spanLogs.isEmpty()) {
      sb.append(' ');
      sb.append(sanitize(SPAN_LOG_KEY));
      sb.append('=');
      sb.append(sanitize("true"));
    }
    sb.append(' ');
    sb.append(startMillis);
    sb.append(' ');
    sb.append(durationMillis);
    // TODO - Support SpanLogs
    sb.append('\n');
    return sb.toString();
  }

  public static String eventToLineData(String name, long startMillis, long endMillis,
                                       @Nullable String source, @Nullable Map tags,
                                       @Nullable Map annotations,
                                       String defaultSource, boolean jsonify)
      throws JsonProcessingException {

    /* Wavefront Event Data format
     *
     * Proxy: @Event    [annotations] host=source [tags]
     * Direct Ingestion: JSON string
     *
     * Example:
     * Direct Ingestion:
     *                    {
     *                      "name": "Event Example",
     *                      "hosts": ["localhost"]
     *                      "annotations": {
     *                        "severity": "info",
     *                        "type": "event type",
     *                        "details": "description"
     *                      },
     *                      "tags" : ["key1: value1"],
     *                      "startTime": 1598571847285,
     *                      "endTime": 1598571847286
     *                    }
     * Proxy: @Event 1598571847285 1598571847286 "Event Example" details="description"
     *        type="event type" severity="severity" host="localhost" tag="key1: value1"
     */

    // check and sanitize parameters
    if (source == null || source.isEmpty()) {
      source = defaultSource;
    }
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("event name cannot be blank " +
          getContextInfo(name, source, tags));
    }
    if (source == null || source.isEmpty()) {
      throw new IllegalArgumentException("event source and default source cannot be blank " +
          getContextInfo(name, source, tags));
    }

    if (endMillis == 0) {
      endMillis = startMillis + 1;
    }

    List sanitizedTags = null;
    Map sanitizedAnnotations = null;

    if (tags != null) {
      sanitizedTags = new ArrayList<>();
      for (final Map.Entry tag : tags.entrySet()) {
        String key = tag.getKey();
        String val = tag.getValue();
        if (key == null || key.isEmpty()) {
          throw new IllegalArgumentException("event tag key cannot be blank " +
              getContextInfo(name, source, tags));
        }
        if (val == null || val.isEmpty()) {
          throw new IllegalArgumentException("event tag value cannot be blank for " +
              "tag key: " + key + " " + getContextInfo(name, source, tags));
        }
        sanitizedTags.add(sanitizeWithoutQuotes(key) + ": " + val.trim());
      }
    }

    if (annotations != null) {
      sanitizedAnnotations = new HashMap<>();
      for (final Map.Entry annotation : annotations.entrySet()) {
        String key = annotation.getKey();
        String val = annotation.getValue();
        if (key == null || key.isEmpty()) {
          throw new IllegalArgumentException("event annotation key cannot be blank " +
              getContextInfo(name, source, tags));
        }
        if (val == null || val.isEmpty()) {
          throw new IllegalArgumentException("event annotation value cannot be blank for " +
              "annotation key: " + key + " " + getContextInfo(name, source, tags));
        }
        sanitizedAnnotations.put(sanitizeWithoutQuotes(key), val.trim());
      }
    }

    if (jsonify) {
      // convert event to JSON string
      final StringBuilder toReturn = new StringBuilder();
      // For JSON format in direct ingestion, annotations field is required even it is empty
      if (sanitizedAnnotations == null) {
        sanitizedAnnotations = new HashMap<>();
      }
      toReturn.append(JSON_PARSER.writeValueAsString(new EventDTO(name, startMillis, endMillis,
          source, sanitizedAnnotations, sanitizedTags)))
          .append("\n");
      return toReturn.toString();
    }

    // convert event to line format
    final StringBuilder sb = new StringBuilder();

    sb.append("@Event");
    sb.append(' ');
    sb.append(startMillis);
    sb.append(' ');
    sb.append(endMillis);
    sb.append(' ');
    sb.append(sanitizeValue(name));

    if (sanitizedAnnotations != null) {
      for (final Map.Entry annotation : sanitizedAnnotations.entrySet()) {
        sb.append(' ');
        sb.append(annotation.getKey());
        sb.append('=');
        sb.append(sanitizeValue(annotation.getValue()));
      }
    }

    sb.append(" host=");
    sb.append(sanitizeValue(source));

    if (sanitizedTags != null) {
      for (String tag : sanitizedTags) {
        sb.append(" tag=");
        sb.append(sanitizeValue(tag));
      }
    }

    sb.append('\n');
    return sb.toString();
  }

  public static String spanLogsToLineData(UUID traceId, UUID spanId, @NonNull List spanLogs)
      throws JsonProcessingException {
    return spanLogsToLineData(traceId, spanId, spanLogs, null);
  }

  public static String spanLogsToLineData(UUID traceId, UUID spanId,
                                          @NonNull List spanLogs, @Nullable String span)
      throws JsonProcessingException {
    /*
     * Wavefront Span Log Data format
     * Example:
     *  {
     *      "traceId": "7b3bf470-9456-11e8-9eb6-529269fb1459",
     *      "spanId": "0313bafe-9457-11e8-9eb6-529269fb1459",
     *      "logs": [
     *          {
     *              "timestamp": "1533531013",
     *              "fields": {
     *                  "event": "error",
     *                  "error.kind": "exception",
     *                  "message": "timed out",
     *                  "stack": "File \"example.py\", line 7, in \\ncaller()\nFile \"example.py\""
     *              }
     *          }
     *      ]
     *  }
     */

    StringBuilder toReturn = new StringBuilder();
    toReturn.append(JSON_PARSER.writeValueAsString(new SpanLogsDTO(traceId, spanId, spanLogs,
        span)));
    toReturn.append("\n");
    return toReturn.toString();
  }

  public static void shutdownExecutorAndWait(ExecutorService tpe) {
    tpe.shutdown();
    try {
      if (!tpe.awaitTermination(60, TimeUnit.SECONDS)) {
        tpe.shutdownNow();
        if (!tpe.awaitTermination(60, TimeUnit.SECONDS))
          logger.log(Level.FINE, "pool did not terminate");
      }
    } catch (InterruptedException ie) {
      tpe.shutdownNow();
      Thread.currentThread().interrupt();
    }
  }
  
  private static String sanitizeInternal(String s, boolean addQuotes, boolean ignoreSlash) {
    /*
     * Sanitize string of metric name, source and key of tags according to the rule of Wavefront proxy.
     */

    StringBuilder sb = new StringBuilder();
    if (addQuotes) {
      sb.append('"');
    }
    for (int i = 0; i < s.length(); i++) {
      char cur = s.charAt(i);
      boolean isLegal = true;
      boolean isTildaPrefixed = s.charAt(0) == 126;
      boolean isDeltaPrefixed = (s.charAt(0) == 0x2206) || (s.charAt(0) == 0x0394);
      boolean isDeltaTildaPrefixed = isDeltaPrefixed && s.charAt(1) == 126;
      if (!(44 <= cur && cur <= 57) && !(65 <= cur && cur <= 90) && !(97 <= cur && cur <= 122) &&
          cur != 95) {
        if (!(i == 0 && (isDeltaPrefixed || isTildaPrefixed) || (i == 1 && isDeltaTildaPrefixed))) {
          // first character can also be \u2206 (∆ - INCREMENT) or \u0394 (Δ - GREEK CAPITAL LETTER DELTA)
          // or ~ tilda character for internal metrics
          // second character can be ~ tilda character if first character is \u2206 (∆ - INCREMENT)
          // or \u0394 (Δ - GREEK CAPITAL LETTER DELTA)
          isLegal = false;
        }
      }
      if (cur == '/' && !ignoreSlash) {
        isLegal = false;
      }
      sb.append(isLegal ? cur : '-');
    }
    if (addQuotes) {
      sb.append('"');
    }
    return sb.toString();
  }

  /**
   * Builds a best-effort string representation of a metric/histogram/span/event to provide additional
   * context to error messages. This implementation doesn't have to be as strict about escaping
   * values, since this is for internal use only. This method swallows all exceptions so it's
   * safe to use in critical blocks.
   *
   * @param name   Entity name
   * @param source Source name
   * @param tags   A collection of tags, either as {@code List>} or
   *               {@code Map}.
   * @return best-effort string representation or an empty string if it's not possible
   */
  @SuppressWarnings("unchecked")
  private static String getContextInfo(@Nullable String name, @Nullable String source,
                                       @Nullable Object tags) {
    try {
      StringBuilder sb = new StringBuilder("(");
      if (name != null) sb.append(name);
      if (source != null) sb.append(" source=").append(source);
      if (tags != null) {
        if (tags instanceof Map) {
          for (Map.Entry entry : ((Map) tags).entrySet()) {
            sb.append(' ').append(entry.getKey()).append("=[").append(entry.getValue()).append(']');
          }
        } else if (tags instanceof List) {
          for (Pair entry : (List>) tags) {
            sb.append(' ').append(entry._1).append("=[").append(entry._2).append(']');
          }
        }
      }
      sb.append(")");
      return sb.toString();
    } catch (Exception ex) {
      return "";
    }
  }

  private static void appendCompactedCentroids(StringBuilder sb,
                                               List> centroids) {
    Double accumulatedValue = null;
    int accumulatedCount = 0;
    for (Pair centroid : centroids) {
      double value = centroid._1;
      int count = centroid._2;
      if (accumulatedValue != null && value != accumulatedValue) {
        sb.append('#').append(accumulatedCount).append(' ');
        sb.append(accumulatedValue).append(' ');
        accumulatedValue = value;
        accumulatedCount = count;
      } else {
        if (accumulatedValue == null) {
          accumulatedValue = value;
        }
        accumulatedCount += count;
      }
    }
    if (accumulatedValue != null) {
      sb.append('#').append(accumulatedCount).append(' ');
      sb.append(accumulatedValue).append(' ');
    }
  }

  /**
   * Return the resource at the given resource path in the resource directory
   * {@code META-INF/}.
   *
   * @param pathToResource
   * @return the resource at the given path, if exists.
   */
  public static Optional getResource(String pathToResource) {
    if (pathToResource != null && !pathToResource.isEmpty()) {
      ResourceBundle resourceBundle = null;
      try {
        resourceBundle = ResourceBundle.getBundle(RESOURCES_ROOT + pathToResource);
      } catch (Exception ex) {
        logger.log(Level.WARNING, ex.getMessage());
      }
      if (resourceBundle != null) {
        return Optional.of(resourceBundle);
      }
    }
    return Optional.empty();
  }

  /**
   * Return the version of the given artifact Id.
   * This depends on a file with name "<artifactId>.properties" in the resource directory
   * {@code META-INF/}. The file should contain a line that specifies the project version as
   * "project.version".
   *
   * @param artifactId
   * @return the version for the given artifactId
   */
  public static Optional getVersion(String artifactId) {
    if (artifactId != null && !artifactId.isEmpty()) {
      Optional resource = getResource(artifactId + "/build");
      if (resource.isPresent()) {
        ResourceBundle resourceBundle = resource.get();
        if (resourceBundle.containsKey(VERSION)) {
          String version = resourceBundle.getString(VERSION);
          if (version != null && !version.isEmpty()) {
            return Optional.of(version);
          }
        }
      }
    }
    return Optional.empty();
  }

  /**
   * Return the version of the given artifact Id as a gauge value to be reported to Wavefront.
   * This depends on a file with name "<artifactId>.properties" in the resource directory
   * {@code META-INF/}. The file should contain a line that specifies the project version as
   * "project.version".
   * Format of semantic version gauge value reported to Wavefront =
   * .<2-digit-minorVersion><2-digit-patchVersion>
   *
   * Ex: v2.6.1 => 2.0601
   *
   * @param artifactId
   * @return the version Gauge value for the given artifactId
   */
  public static double getSemVerGauge(String artifactId) {
    Optional version = getVersion(artifactId);
    if (version.isPresent()) {
      return convertSemVerToGauge(version.get());
    } else {
      logger.log(Level.INFO, "Could not retrieve build version info for : ", artifactId);
    }
    return 0.0D;
  }

  public static double convertSemVerToGauge(String version) {
    if (version != null && !version.isEmpty()) {
      Matcher semVerMatcher = SEMVER_PATTERN.matcher(version);
      if (semVerMatcher.matches()) {
        // Major version
        StringBuilder sdkVersion = new StringBuilder(semVerMatcher.group(1));
        sdkVersion.append(".");
        String minor = semVerMatcher.group(2);
        String patch = semVerMatcher.group(3);
        String snapshot = semVerMatcher.group(4);
        // Minor version
        if (minor.length() == 1) {
          sdkVersion.append("0" + minor);
        } else {
          sdkVersion.append(minor);
        }
        // Patch Version
        if (patch.length() == 1) {
          sdkVersion.append("0" + patch);
        } else {
          sdkVersion.append(patch);
        }
        try {
          return Double.valueOf(sdkVersion.toString());
        } catch (Exception ex) {
          logger.log(Level.INFO, ex.getMessage());
        }
      }
    }
    return 0.0D;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy