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

com.wavefront.ingester.IngesterFormatter Maven / Gradle / Ivy

There is a newer version: 4.8
Show newest version
package com.wavefront.ingester;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import com.wavefront.common.Clock;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import queryserver.parser.DSWrapperLexer;
import sunnylabs.report.ReportPoint;

/**
 * Builder pattern for creating new ingestion formats. Inspired by the date time formatters in
 * Joda.
 *
 * @author Clement Pang ([email protected]).
 */
public class IngesterFormatter {

  private static final FormatterElement WHITESPACE_ELEMENT = new Whitespace();
  private static final Pattern SINGLE_QUOTE_PATTERN = Pattern.compile("\\'", Pattern.LITERAL);
  private static final Pattern DOUBLE_QUOTE_PATTERN = Pattern.compile("\\\"", Pattern.LITERAL);
  private static final String DOUBLE_QUOTE_REPLACEMENT = Matcher.quoteReplacement("\"");
  private static final String SINGLE_QUOTE_REPLACEMENT = Matcher.quoteReplacement("'");

  private static final BaseErrorListener THROWING_ERROR_LISTENER = new BaseErrorListener() {
    @Override
    public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line,
                            int charPositionInLine, String msg, RecognitionException e) {
      throw new RuntimeException(msg, e);
    }
  };

  private final List elements;
  private final ThreadLocal dsWrapperLexerThreadLocal =
      new ThreadLocal() {
        @Override
        protected DSWrapperLexer initialValue() {
          final DSWrapperLexer lexer = new DSWrapperLexer(new ANTLRInputStream(""));
          // note that other errors are not thrown by the lexer and hence we only need to handle the
          // syntaxError case.
          lexer.removeErrorListeners();
          lexer.addErrorListener(THROWING_ERROR_LISTENER);
          return lexer;
        }
      };

  private IngesterFormatter(List elements) {
    this.elements = elements;
  }

  public ReportPoint drive(String input, String defaultHostName, String customerId,
                           List customSourceTags) {
    DSWrapperLexer lexer = dsWrapperLexerThreadLocal.get();
    lexer.setInputStream(new ANTLRInputStream(input));
    CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
    commonTokenStream.getText();
    List tokens = commonTokenStream.getTokens();
    if (tokens.isEmpty()) {
      throw new RuntimeException("Could not parse: " + input);
    }
    // this is sensitive to the grammar in DSQuery.g4. We could just use the visitor but doing so
    // means we need to be creating the AST and instead we could just use the lexer. in any case,
    // we don't expect the graphite format to change anytime soon.

    // filter all EOF tokens first.
    Queue queue = new ArrayDeque<>(Lists.newArrayList(Iterables.filter(tokens,
        new Predicate() {
          @Override
          public boolean apply(Token input) {
            return input.getType() != Lexer.EOF;
          }
        })));
    ReportPoint point = new ReportPoint();
    point.setTable(customerId);
    // if the point has a timestamp, this would be overriden
    point.setTimestamp(Clock.now());
    try {
      for (FormatterElement element : elements) {
        element.consume(queue, point);
      }
    } catch (Exception ex) {
      throw new RuntimeException("Could not parse: " + input, ex);
    }
    if (!queue.isEmpty()) {
      throw new RuntimeException("Could not parse: " + input);
    }

    String host = null;
    Map annotations = point.getAnnotations();
    if (annotations != null) {
      host = annotations.remove("source");
      if (host == null) {
        host = annotations.remove("host");
      } else if (annotations.containsKey("host")) {
        // we have to move this elsewhere since during querying,
        // host= would be interpreted as host and not a point tag
        annotations.put("_host", annotations.remove("host"));
      }
      if (annotations.containsKey("tag")) {
        annotations.put("_tag", annotations.remove("tag"));
      }
      if (host == null) {
        // iterate over the set of custom tags, breaking when one is found
        for (String tag : customSourceTags) {
          host = annotations.get(tag);
          if (host != null) {
            break;
          }
        }
      }
    }
    if (host == null) {
      host = defaultHostName;
    }
    point.setHost(host);
    return point;
  }

  public static IngesterFormatBuilder newBuilder() {
    return new IngesterFormatBuilder();
  }

  public static class IngesterFormatBuilder {
    private final List elements = Lists.newArrayList();

    public IngesterFormatBuilder appendCaseSensitiveLiteral(String literal) {
      elements.add(new Literal(literal, true));
      return this;
    }

    public IngesterFormatBuilder appendCaseInsensitiveLiteral(String literal) {
      elements.add(new Literal(literal, false));
      return this;
    }

    public IngesterFormatBuilder appendMetricName() {
      elements.add(new Metric());
      return this;
    }

    public IngesterFormatBuilder appendValue() {
      elements.add(new Value());
      return this;
    }

    public IngesterFormatBuilder appendTimestamp() {
      elements.add(new AdaptiveTimestamp(false));
      return this;
    }

    public IngesterFormatBuilder appendOptionalTimestamp() {
      elements.add(new AdaptiveTimestamp(true));
      return this;
    }

    public IngesterFormatBuilder appendTimestamp(TimeUnit timeUnit) {
      elements.add(new Timestamp(timeUnit, false));
      return this;
    }

    public IngesterFormatBuilder appendOptionalTimestamp(TimeUnit timeUnit) {
      elements.add(new Timestamp(timeUnit, true));
      return this;
    }

    public IngesterFormatBuilder appendAnnotationsConsumer() {
      elements.add(new Loop(new Tag()));
      return this;
    }

    public IngesterFormatBuilder whiteSpace() {
      elements.add(new Whitespace());
      return this;
    }

    public IngesterFormatter build() {
      return new IngesterFormatter(elements);
    }
  }

  private interface FormatterElement {
    /**
     * Consume tokens from the queue.
     */
    void consume(Queue tokenQueue, ReportPoint point);
  }

  public static class Loop implements FormatterElement {

    private final FormatterElement element;

    public Loop(FormatterElement element) {
      this.element = element;
    }

    @Override
    public void consume(Queue tokenQueue, ReportPoint point) {
      while (!tokenQueue.isEmpty()) {
        WHITESPACE_ELEMENT.consume(tokenQueue, point);
        if (tokenQueue.isEmpty()) return;
        element.consume(tokenQueue, point);
      }
    }
  }

  public static class Tag implements FormatterElement {

    @Override
    public void consume(Queue queue, ReportPoint point) {
      // extract tags.
      String tagk;
      tagk = getLiteral(queue);
      if (tagk.length() == 0) {
        throw new RuntimeException("Invalid tag name");
      }
      WHITESPACE_ELEMENT.consume(queue, point);
      Token current = queue.poll();
      if (current == null || current.getType() != DSWrapperLexer.EQ) {
        throw new RuntimeException("Tag keys and values must be separated by '='" +
            (current != null ? ", " + "found: " + current.getText() : ", found EOF"));
      }
      WHITESPACE_ELEMENT.consume(queue, point);
      String tagv = getLiteral(queue);
      if (tagv.length() == 0) throw new RuntimeException("Invalid tag value for: " + tagk);
      if (point.getAnnotations() == null) {
        point.setAnnotations(Maps.newHashMap());
      }
      point.getAnnotations().put(tagk, tagv);
    }
  }

  public static class Value implements FormatterElement {

    @Override
    public void consume(Queue tokenQueue, ReportPoint point) {
      String value = "";
      Token current = tokenQueue.poll();
      if (current == null) throw new RuntimeException("Invalid metric value, found EOF");
      if (current.getType() == DSWrapperLexer.MinusSign) {
        current = tokenQueue.poll();
        value = "-";
      }
      if (current == null) throw new RuntimeException("Invalid metric value, found EOF");
      if (current.getType() == DSWrapperLexer.Quoted) {
        if (!value.equals("")) {
          throw new RuntimeException("invalid metric value: " + value + current.getText());
        }
        value += unquote(current.getText());
      } else if (current.getType() == DSWrapperLexer.Letters ||
          current.getType() == DSWrapperLexer.Literal ||
          current.getType() == DSWrapperLexer.Number) {
        value += current.getText();
      } else {
        throw new RuntimeException("invalid metric value: " + current.getText());
      }
      try {
        point.setValue(Double.parseDouble(value));
      } catch (NumberFormatException nef) {
        throw new RuntimeException("invalid metric value: " + value);
      }
    }
  }

  public static class AdaptiveTimestamp implements FormatterElement {

    private final boolean optional;

    public AdaptiveTimestamp(boolean optional) {
      this.optional = optional;
    }

    @Override
    public void consume(Queue tokenQueue, ReportPoint point) {
      Token peek = tokenQueue.peek();
      if (peek == null) {
        if (optional) return;
        else throw new RuntimeException("Expecting timestamp, found EOF");
      }
      if (peek.getType() == DSWrapperLexer.Number) {
        try {
          Double timestamp = Double.parseDouble(tokenQueue.poll().getText());
          Long timestampLong = timestamp.longValue();
          // see if it has 13 digits.
          if (timestampLong.toString().length() == 13) {
            // milliseconds.
            point.setTimestamp(timestamp.longValue());
          } else {
            // treat it as seconds.
            point.setTimestamp((long) (1000.0 * timestamp));
          }
        } catch (NumberFormatException nfe) {
          throw new RuntimeException("Invalid timestamp value: " + peek.getText());
        }
      } else if (!optional) {
        throw new RuntimeException("Expecting timestamp, found: " + peek.getText());
      }
    }
  }

  public static class Timestamp implements FormatterElement {

    private final TimeUnit timeUnit;
    private final boolean optional;

    public Timestamp(TimeUnit timeUnit, boolean optional) {
      this.timeUnit = timeUnit;
      this.optional = optional;
    }

    @Override
    public void consume(Queue tokenQueue, ReportPoint point) {
      Token peek = tokenQueue.peek();
      if (peek == null) {
        if (optional) return;
        else throw new RuntimeException("Expecting timestamp, found EOF");
      }
      if (peek.getType() == DSWrapperLexer.Number) {
        try {
          // we need to handle the conversion outselves.
          long multiplier = timeUnit.toMillis(1);
          if (multiplier < 1) {
            point.setTimestamp(timeUnit.toMillis(
                (long) Double.parseDouble(tokenQueue.poll().getText())));
          } else {
            point.setTimestamp((long)
                (multiplier * Double.parseDouble(tokenQueue.poll().getText())));
          }
        } catch (NumberFormatException nfe) {
          throw new RuntimeException("Invalid timestamp value: " + peek.getText());
        }
      } else if (!optional) {
        throw new RuntimeException("Expecting timestamp, found: " + peek.getText());
      }
    }
  }

  public static class Metric implements FormatterElement {

    @Override
    public void consume(Queue tokenQueue, ReportPoint point) {
      // extract the metric name.
      String metric = getLiteral(tokenQueue);
      if (metric.length() == 0) throw new RuntimeException("Invalid metric name");
      point.setMetric(metric);
    }
  }

  public static class Whitespace implements FormatterElement {
    @Override
    public void consume(Queue tokens, ReportPoint point) {
      while (!tokens.isEmpty() && tokens.peek().getType() == DSWrapperLexer.WS) {
        tokens.poll();
      }
    }
  }

  public static class Literal implements FormatterElement {

    private final String literal;
    private final boolean caseSensitive;

    public Literal(String literal, boolean caseSensitive) {
      this.literal = literal;
      this.caseSensitive = caseSensitive;
    }

    @Override
    public void consume(Queue tokenQueue, ReportPoint point) {
      if (tokenQueue.isEmpty()) {
        throw new RuntimeException("Expecting a literal string: " + literal + " but found EOF");
      }
      String literal = getLiteral(tokenQueue);
      if (caseSensitive) {
        if (!literal.equals(this.literal)) {
          throw new RuntimeException("Expecting a literal string: " + literal + " but found: " +
              literal);
        }
      } else {
        if (!literal.equalsIgnoreCase(this.literal)) {
          throw new RuntimeException("Expecting a literal string: " + literal + " but found: " +
              literal);
        }
      }
    }
  }

  private static String getLiteral(Queue tokens) {
    String toReturn = "";
    Token next = tokens.peek();
    if (next == null) return "";
    if (next.getType() == DSWrapperLexer.Quoted) {
      return unquote(tokens.poll().getText());
    }
    while (next != null && (next.getType() == DSWrapperLexer.Letters ||
        next.getType() == DSWrapperLexer.Number ||
        next.getType() == DSWrapperLexer.Literal ||
        next.getType() == DSWrapperLexer.IpV4Address ||
        next.getType() == DSWrapperLexer.MinusSign ||
        next.getType() == DSWrapperLexer.IpV6Address)) {
      toReturn += tokens.poll().getText();
      next = tokens.peek();
    }
    return toReturn;
  }

  public static String unquote(String text) {
    if (text.startsWith("\"")) {
      text = DOUBLE_QUOTE_PATTERN.matcher(text.substring(1, text.length() - 1)).
          replaceAll(DOUBLE_QUOTE_REPLACEMENT);
    } else if (text.startsWith("'")) {
      text = SINGLE_QUOTE_PATTERN.matcher(text.substring(1, text.length() - 1)).
          replaceAll(SINGLE_QUOTE_REPLACEMENT);
    }
    return text;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy