001package io.prometheus.metrics.expositionformats;
002
003import io.prometheus.metrics.config.EscapingScheme;
004import io.prometheus.metrics.model.snapshots.Labels;
005import io.prometheus.metrics.model.snapshots.PrometheusNaming;
006import io.prometheus.metrics.model.snapshots.SnapshotEscaper;
007import java.io.IOException;
008import java.io.Writer;
009import javax.annotation.Nullable;
010
011public class TextFormatUtil {
012
013  static void writeLong(Writer writer, long value) throws IOException {
014    writer.append(Long.toString(value));
015  }
016
017  static void writeDouble(Writer writer, double d) throws IOException {
018    if (d == Double.POSITIVE_INFINITY) {
019      writer.write("+Inf");
020    } else if (d == Double.NEGATIVE_INFINITY) {
021      writer.write("-Inf");
022    } else {
023      writer.write(Double.toString(d));
024      // FloatingDecimal.getBinaryToASCIIConverter(d).appendTo(writer);
025    }
026  }
027
028  static void writePrometheusTimestamp(Writer writer, long timestampMs, boolean timestampsInMs)
029      throws IOException {
030    if (timestampsInMs) {
031      // correct for prometheus exposition format
032      // https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details
033      writer.write(Long.toString(timestampMs));
034    } else {
035      // incorrect for prometheus exposition format -
036      // but we need to support it for backwards compatibility
037      writeOpenMetricsTimestamp(writer, timestampMs);
038    }
039  }
040
041  static void writeOpenMetricsTimestamp(Writer writer, long timestampMs) throws IOException {
042    writer.write(Long.toString(timestampMs / 1000L));
043    writer.write(".");
044    long ms = timestampMs % 1000;
045    if (ms < 100) {
046      writer.write("0");
047    }
048    if (ms < 10) {
049      writer.write("0");
050    }
051    writer.write(Long.toString(ms));
052  }
053
054  static void writeEscapedString(Writer writer, String s) throws IOException {
055    // optimize for the common case where no escaping is needed
056    int start = 0;
057    // #indexOf is a vectorized intrinsic
058    int backslashIndex = s.indexOf('\\', start);
059    int quoteIndex = s.indexOf('\"', start);
060    int newlineIndex = s.indexOf('\n', start);
061
062    int allEscapesIndex = backslashIndex & quoteIndex & newlineIndex;
063    while (allEscapesIndex != -1) {
064      int escapeStart = Integer.MAX_VALUE;
065      if (backslashIndex != -1) {
066        escapeStart = backslashIndex;
067      }
068      if (quoteIndex != -1) {
069        escapeStart = Math.min(escapeStart, quoteIndex);
070      }
071      if (newlineIndex != -1) {
072        escapeStart = Math.min(escapeStart, newlineIndex);
073      }
074
075      // bulk write up to the first character that needs to be escaped
076      if (escapeStart > start) {
077        writer.write(s, start, escapeStart - start);
078      }
079      char c = s.charAt(escapeStart);
080      start = escapeStart + 1;
081      switch (c) {
082        case '\\':
083          writer.write("\\\\");
084          backslashIndex = s.indexOf('\\', start);
085          break;
086        case '\"':
087          writer.write("\\\"");
088          quoteIndex = s.indexOf('\"', start);
089          break;
090        case '\n':
091          writer.write("\\n");
092          newlineIndex = s.indexOf('\n', start);
093          break;
094      }
095
096      allEscapesIndex = backslashIndex & quoteIndex & newlineIndex;
097    }
098    // up until the end nothing needs to be escaped anymore
099    int remaining = s.length() - start;
100    if (remaining > 0) {
101      writer.write(s, start, remaining);
102    }
103  }
104
105  static void writeLabels(
106      Writer writer,
107      Labels labels,
108      @Nullable String additionalLabelName,
109      double additionalLabelValue,
110      boolean metricInsideBraces,
111      EscapingScheme scheme)
112      throws IOException {
113    if (!metricInsideBraces) {
114      writer.write('{');
115    }
116    for (int i = 0; i < labels.size(); i++) {
117      if (i > 0 || metricInsideBraces) {
118        writer.write(",");
119      }
120      writeName(writer, SnapshotEscaper.getSnapshotLabelName(labels, i, scheme), NameType.Label);
121      writer.write("=\"");
122      writeEscapedString(writer, labels.getValue(i));
123      writer.write("\"");
124    }
125    if (additionalLabelName != null) {
126      if (!labels.isEmpty()) {
127        writer.write(",");
128      }
129      writer.write(additionalLabelName);
130      writer.write("=\"");
131      writeDouble(writer, additionalLabelValue);
132      writer.write("\"");
133    }
134    writer.write('}');
135  }
136
137  static void writeName(Writer writer, String name, NameType nameType) throws IOException {
138    switch (nameType) {
139      case Metric:
140        if (PrometheusNaming.isValidLegacyMetricName(name)) {
141          writer.write(name);
142          return;
143        }
144        break;
145      case Label:
146        if (PrometheusNaming.isValidLegacyLabelName(name)) {
147          writer.write(name);
148          return;
149        }
150        break;
151      default:
152        throw new RuntimeException("Invalid name type requested: " + nameType);
153    }
154    writer.write('"');
155    writeEscapedString(writer, name);
156    writer.write('"');
157  }
158}