zipkin源码修改使得存入到es的数据可以添加自定义数据mapping属性支持检索

场景:项目中通过sleuth+zipkin进行链路追踪,为了适应项目需要在存入的es的json数据中添加自定义数据便于进行es检索操作。
项目收集通过es存储数据 kafka进行收集

1、在项目中引入sleuth zipkin依赖

	<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
       

2、客户端通过sleuth中的方式进行客户端链路信息的扩展

@Configuration(proxyBeanMethods = false)
public class ClientParserConfiguration {

    @Value("${spring.application.logyname:}")
    private String belLogy;
    @Value("${spring.application.appname:}")
    private String belApp;
    @Value("${server.port:}")
    private String port;
    @Value("${spring.application.name:}")
    private String belSer;
    @Value("${server.port:}")
    private String localEndPointPort;
    @Value("${eureka.instance.ip-address:}")
    private String localEndPointIp;

    @Bean(name = HttpClientRequestParser.NAME)
    HttpRequestParser myHttpClientRequestParser() {
        return (request, context, span) -> {
            span.name(request.method());
            span.tag("belLogy", belLogy);
            span.tag("belApp", belApp);
            span.tag("belSer", belSer);
            span.tag("serverName", belSer + port);
            span.tag("localEndPointIp", localEndPointIp);
            span.tag("localEndPointPort", localEndPointPort);

            String method = request.method();
            String path = request.path();
            span.tag("http.method", method);
            span.tag("http.path", path);
        };
    }

    @Bean(name = HttpClientResponseParser.NAME)
    HttpResponseParser myHttpClientResponseParser() {
        return (response, context, span) -> {
            span.tag("belLogy", belLogy);
            span.tag("belApp", belApp);
            span.tag("belSer", belSer);
            span.tag("serverName", belSer + port);
            span.tag("localEndPointIp", localEndPointIp);
            span.tag("localEndPointPort", localEndPointPort);

            String method = response.method();
            String path = response.request().path();
            span.tag("http.method", method);
            span.tag("http.path", path);
        };
    }
}

@Configuration(proxyBeanMethods = false)
public class ServerParserConfiguration {

    @Value("${spring.application.logyname:}")
    private String belLogy;

    @Value("${spring.application.appname:}")
    private String belApp;

    @Value("${server.port:}")
    private String port;

    @Value("${spring.application.name:}")
    private String belSer;

    @Value("${server.port:}")
    private String localEndPointPort;

    @Value("${eureka.instance.ip-address:}")
    private String localEndPointIp;

    @Bean(name = HttpServerRequestParser.NAME)
    HttpRequestParser myHttpClientRequestParser() {
        return (request, context, span) -> {
            span.name(request.method());
            span.tag("belLogy", belLogy);
            span.tag("belApp", belApp);
            span.tag("belSer", belSer);
            span.tag("serverName", belSer + port);
            span.tag("localEndPointIp", localEndPointIp);
            span.tag("localEndPointPort", localEndPointPort);

            String method = request.method();
            String path = request.path();
            span.tag("http.method", method);
            span.tag("http.path", path);
        };
    }

    @Bean(name = HttpServerResponseParser.NAME)
    HttpResponseParser myHttpClientResponseParser() {
        return (response, context, span) -> {
            span.tag("belLogy", belLogy);
            span.tag("belApp", belApp);
            span.tag("belSer", belSer);
            span.tag("serverName", belSer + port);
            span.tag("localEndPointIp", localEndPointIp);
            span.tag("localEndPointPort", localEndPointPort);

            String method = response.method();
            String path = response.request().path();
            span.tag("http.method", method);
            span.tag("http.path", path);
        };
    }
}

@Configuration
@AutoConfigureBefore(TraceSpringIntegrationAutoConfiguration.class)
public class SleuthConfig {
    @Value("${spring.application.logyname:}")
    private String belLogy;

    @Value("${spring.application.appname:}")
    private String belApp;

    @Value("${spring.application.name:}")
    private String belSer;

    @Value("${server.port:}")
    private String port;

    @Value("${server.port:}")
    private String localEndPointPort;

    @Value("${eureka.instance.ip-address:}")
    private String localEndPointIp;

    @Bean
    SpanHandler handlerOne() {
        return new SpanHandler() {
            @Override
            public boolean end(TraceContext traceContext, MutableSpan span,
                               Cause cause) {
                String method = span.tag("mvc.controller.method");
                String path = span.tag("http.path");
                //  String targetPath = StringUtils.isEmpty(path) ? path : method;
                // 接口方法与http调用方法优先使用接口方法
                String targetPath = StringUtils.isEmpty(method) ? path : method;
                span.tag("http.path", targetPath);
                span.tag("belSer", belSer);
                span.tag("belLogy", belLogy);
                span.tag("belApp", belApp);
                span.tag("serverName", belSer + port);
                span.tag("localEndPointIp", localEndPointIp);
                span.tag("localEndPointPort", localEndPointPort);
                return true;
            }
        };
    }
}

3、修改zipkin源码在替换到zipkin的jar中

public final class V2SpanReader implements JsonReaderAdapter<Span> {
  Span.Builder builder;

  @Override public Span fromJson(JsonReader reader) throws IOException {
    if (builder == null) {
      builder = Span.newBuilder();
    } else {
      builder.clear();
    }
    reader.beginObject();
    while (reader.hasNext()) {
      String nextName = reader.nextName();
      if (nextName.equals("traceId")) {
        builder.traceId(reader.nextString());
        continue;
      } else if (nextName.equals("id")) {
        builder.id(reader.nextString());
        continue;
      } else if (reader.peekNull()) {
        reader.skipValue();
        continue;
      }

      // read any optional fields
      if (nextName.equals("parentId")) {
        builder.parentId(reader.nextString());
      } else if (nextName.equals("kind")) {
        builder.kind(Span.Kind.valueOf(reader.nextString()));
      } else if (nextName.equals("name")) {
        builder.name(reader.nextString());
      } else if (nextName.equals("timestamp")) {
        builder.timestamp(reader.nextLong());
      } else if (nextName.equals("duration")) {
        builder.duration(reader.nextLong());
      } else if (nextName.equals("localEndpoint")) {
        builder.localEndpoint(ENDPOINT_READER.fromJson(reader));
      } else if (nextName.equals("remoteEndpoint")) {
        builder.remoteEndpoint(ENDPOINT_READER.fromJson(reader));
      } else if (nextName.equals("annotations")) {
        reader.beginArray();
        while (reader.hasNext()) {
          reader.beginObject();
          Long timestamp = null;
          String value = null;
          while (reader.hasNext()) {
            nextName = reader.nextName();
            if (nextName.equals("timestamp")) {
              timestamp = reader.nextLong();
            } else if (nextName.equals("value")) {
              value = reader.nextString();
            } else {
              reader.skipValue();
            }
          }
          if (timestamp == null || value == null) {
            throw new IllegalArgumentException("Incomplete annotation at " + reader.getPath());
          }
          reader.endObject();
          builder.addAnnotation(timestamp, value);
        }
        reader.endArray();
      } else if (nextName.equals("tags")) {
        reader.beginObject();
        while (reader.hasNext()) {
          String key = reader.nextName();
          if (reader.peekNull()) {
            throw new IllegalArgumentException("No value at " + reader.getPath());
          }
          String value = reader.nextString();
          if ("belApp".equalsIgnoreCase(key)){
            builder.belApp(value);
          }
          if ("belSer".equalsIgnoreCase(key)){
            builder.belSer(value);
          }
          if ("belLogy".equalsIgnoreCase(key)){
            builder.belLogy(value);
          }
          builder.putTag(key, value);
        }
        reader.endObject();
      } else if (nextName.equals("debug")) {
        if (reader.nextBoolean()) builder.debug(true);
      } else if (nextName.equals("shared")) {
        if (reader.nextBoolean()) builder.shared(true);
      } else {
        reader.skipValue();
      }
    }
    reader.endObject();
    return builder.build();
  }

  @Override public String toString() {
    return "Span";
  }

  static final JsonReaderAdapter<Endpoint> ENDPOINT_READER = new JsonReaderAdapter<Endpoint>() {
    @Override public Endpoint fromJson(JsonReader reader) throws IOException {
      Endpoint.Builder result = Endpoint.newBuilder();
      reader.beginObject();
      boolean readField = false;
      while (reader.hasNext()) {
        String nextName = reader.nextName();
        if (reader.peekNull()) {
          reader.skipValue();
          continue;
        }
        if (nextName.equals("serviceName")) {
          result.serviceName(reader.nextString());
          readField = true;
        } else if (nextName.equals("ipv4") || nextName.equals("ipv6")) {
          result.parseIp(reader.nextString());
          readField = true;
        } else if (nextName.equals("port")) {
          result.port(reader.nextInt());
          readField = true;
        } else {
          reader.skipValue();
        }
      }
      reader.endObject();
      return readField ? result.build() : null;
    }

    @Override public String toString() {
      return "Endpoint";
    }
  };
}

public final class V2SpanWriter implements WriteBuffer.Writer<Span> {
  @Override public int sizeInBytes(Span value) {
    String belApp="";
    String belSer="";
    String belLogy="";
    int sizeInBytes = 13; // {"traceId":""
    sizeInBytes += value.traceId().length();
    if (value.parentId() != null) {
      sizeInBytes += 30; // ,"parentId":"0123456789abcdef"
    }
    sizeInBytes += 24; // ,"id":"0123456789abcdef"
    if (value.kind() != null) {
      sizeInBytes += 10; // ,"kind":""
      sizeInBytes += value.kind().name().length();
    }
    if (value.name() != null) {
      sizeInBytes += 10; // ,"name":""
      sizeInBytes += jsonEscapedSizeInBytes(value.name());
    }
    if (value.timestampAsLong() != 0L) {
      sizeInBytes += 13; // ,"timestamp":
      sizeInBytes += asciiSizeInBytes(value.timestampAsLong());
    }
    if (value.durationAsLong() != 0L) {
      sizeInBytes += 12; // ,"duration":
      sizeInBytes += asciiSizeInBytes(value.durationAsLong());
    }
    if (value.localEndpoint() != null) {
      sizeInBytes += 17; // ,"localEndpoint":
      sizeInBytes += endpointSizeInBytes(value.localEndpoint(), false);
    }
    if (value.remoteEndpoint() != null) {
      sizeInBytes += 18; // ,"remoteEndpoint":
      sizeInBytes += endpointSizeInBytes(value.remoteEndpoint(), false);
    }
    if (!value.annotations().isEmpty()) {
      sizeInBytes += 17; // ,"annotations":[]
      int length = value.annotations().size();
      if (length > 1) sizeInBytes += length - 1; // comma to join elements
      for (int i = 0; i < length; i++) {
        Annotation a = value.annotations().get(i);
        sizeInBytes += annotationSizeInBytes(a.timestamp(), a.value(), 0);
      }
    }
    if (!value.tags().isEmpty()) {
      sizeInBytes += 10; // ,"tags":{}
      int tagCount = value.tags().size();
      if (tagCount > 1) sizeInBytes += tagCount - 1; // comma to join elements
      for (Map.Entry<String, String> entry : value.tags().entrySet()) {
        sizeInBytes += 5; // "":""
        sizeInBytes += jsonEscapedSizeInBytes(entry.getKey());
        sizeInBytes += jsonEscapedSizeInBytes(entry.getValue());
        if ("belApp".equalsIgnoreCase(entry.getKey())){
          belApp= entry.getValue();
        }
        if ("belSer".equalsIgnoreCase(entry.getKey())){
          belSer= entry.getValue();
        }
        if ("belLogy".equalsIgnoreCase(entry.getKey())){
          belLogy= entry.getValue();
        }
      }
    }
    if (belApp != null) {
      sizeInBytes += 13; // "belApp":""
      sizeInBytes += jsonEscapedSizeInBytes(belApp);
    }
    if (belSer != null) {
      sizeInBytes += 13; // "belSer":""
      sizeInBytes += jsonEscapedSizeInBytes(belSer);
    }
    if (belLogy != null) {
      sizeInBytes += 14; // "belLogy":""
      sizeInBytes += jsonEscapedSizeInBytes(belLogy);
    }
    if (Boolean.TRUE.equals(value.debug())) {
      sizeInBytes += 13; // ,"debug":true
    }
    if (Boolean.TRUE.equals(value.shared())) {
      sizeInBytes += 14; // ,"shared":true
    }
    return ++sizeInBytes; // }
  }

  @Override public void write(Span value, WriteBuffer b) {
    String belApp="";
    String belSer="";
    String belLogy="";

    b.writeAscii("{\"traceId\":\"");
    b.writeAscii(value.traceId());
    b.writeByte('"');
    if (value.parentId() != null) {
      b.writeAscii(",\"parentId\":\"");
      b.writeAscii(value.parentId());
      b.writeByte('"');
    }
    b.writeAscii(",\"id\":\"");
    b.writeAscii(value.id());
    b.writeByte('"');
    if (value.kind() != null) {
      b.writeAscii(",\"kind\":\"");
      b.writeAscii(value.kind().toString());
      b.writeByte('"');
    }
    if (value.name() != null) {
      b.writeAscii(",\"name\":\"");
      b.writeUtf8(jsonEscape(value.name()));
      b.writeByte('"');
    }
    if (value.timestampAsLong() != 0L) {
      b.writeAscii(",\"timestamp\":");
      b.writeAscii(value.timestampAsLong());
    }
    if (value.durationAsLong() != 0L) {
      b.writeAscii(",\"duration\":");
      b.writeAscii(value.durationAsLong());
    }
    if (value.localEndpoint() != null) {
      b.writeAscii(",\"localEndpoint\":");
      writeEndpoint(value.localEndpoint(), b, false);
    }
    if (value.remoteEndpoint() != null) {
      b.writeAscii(",\"remoteEndpoint\":");
      writeEndpoint(value.remoteEndpoint(), b, false);
    }
    if (!value.annotations().isEmpty()) {
      b.writeAscii(",\"annotations\":");
      b.writeByte('[');
      for (int i = 0, length = value.annotations().size(); i < length; ) {
        Annotation a = value.annotations().get(i++);
        writeAnnotation(a.timestamp(), a.value(), null, b);
        if (i < length) b.writeByte(',');
      }
      b.writeByte(']');
    }
    if (!value.tags().isEmpty()) {
      b.writeAscii(",\"tags\":{");
      Iterator<Map.Entry<String, String>> i = value.tags().entrySet().iterator();
      while (i.hasNext()) {
        Map.Entry<String, String> entry = i.next();
        b.writeByte('"');
        b.writeUtf8(jsonEscape(entry.getKey()));
        b.writeAscii("\":\"");
        b.writeUtf8(jsonEscape(entry.getValue()));
        b.writeByte('"');
        if (i.hasNext()) b.writeByte(',');
        if ("belApp".equalsIgnoreCase(entry.getKey())){
          belApp= entry.getValue();
        }
        if ("belSer".equalsIgnoreCase(entry.getKey())){
          belSer= entry.getValue();
        }
        if ("belLogy".equalsIgnoreCase(entry.getKey())){
          belLogy= entry.getValue();
        }
      }
      b.writeByte('}');
    }
    if (belApp != null) {
      b.writeAscii(",\"belApp\":\"");
      b.writeUtf8(jsonEscape(belApp));
      b.writeByte('"');
    }
    if (belSer != null) {
      b.writeAscii(",\"belSer\":\"");
      b.writeUtf8(jsonEscape(belSer));
      b.writeByte('"');
    }
    if (belLogy != null) {
      b.writeAscii(",\"belLogy\":\"");
      b.writeUtf8(jsonEscape(belLogy));
      b.writeByte('"');
    }
    if (Boolean.TRUE.equals(value.debug())) {
      b.writeAscii(",\"debug\":true");
    }
    if (Boolean.TRUE.equals(value.shared())) {
      b.writeAscii(",\"shared\":true");
    }
    b.writeByte('}');
  }

  @Override public String toString() {
    return "Span";
  }

  static int endpointSizeInBytes(Endpoint value, boolean writeEmptyServiceName) {
    int sizeInBytes = 1; // {
    String serviceName = value.serviceName();
    if (serviceName == null && writeEmptyServiceName) serviceName = "";
    if (serviceName != null) {
      sizeInBytes += 16; // "serviceName":""
      sizeInBytes += jsonEscapedSizeInBytes(serviceName);
    }
    if (value.ipv4() != null) {
      if (sizeInBytes != 1) sizeInBytes++; // ,
      sizeInBytes += 9; // "ipv4":""
      sizeInBytes += value.ipv4().length();
    }
    if (value.ipv6() != null) {
      if (sizeInBytes != 1) sizeInBytes++; // ,
      sizeInBytes += 9; // "ipv6":""
      sizeInBytes += value.ipv6().length();
    }
    int port = value.portAsInt();
    if (port != 0) {
      if (sizeInBytes != 1) sizeInBytes++; // ,
      sizeInBytes += 7; // "port":
      sizeInBytes += asciiSizeInBytes(port);
    }
    return ++sizeInBytes; // }
  }

  static void writeEndpoint(Endpoint value, WriteBuffer b, boolean writeEmptyServiceName) {
    b.writeByte('{');
    boolean wroteField = false;
    String serviceName = value.serviceName();
    if (serviceName == null && writeEmptyServiceName) serviceName = "";
    if (serviceName != null) {
      b.writeAscii("\"serviceName\":\"");
      b.writeUtf8(jsonEscape(serviceName));
      b.writeByte('"');
      wroteField = true;
    }
    if (value.ipv4() != null) {
      if (wroteField) b.writeByte(',');
      b.writeAscii("\"ipv4\":\"");
      b.writeAscii(value.ipv4());
      b.writeByte('"');
      wroteField = true;
    }
    if (value.ipv6() != null) {
      if (wroteField) b.writeByte(',');
      b.writeAscii("\"ipv6\":\"");
      b.writeAscii(value.ipv6());
      b.writeByte('"');
      wroteField = true;
    }
    int port = value.portAsInt();
    if (port != 0) {
      if (wroteField) b.writeByte(',');
      b.writeAscii("\"port\":");
      b.writeAscii(port);
    }
    b.writeByte('}');
  }

  static int annotationSizeInBytes(long timestamp, String value, int endpointSizeInBytes) {
    int sizeInBytes = 25; // {"timestamp":,"value":""}
    sizeInBytes += asciiSizeInBytes(timestamp);
    sizeInBytes += jsonEscapedSizeInBytes(value);
    if (endpointSizeInBytes != 0) {
      sizeInBytes += 12; // ,"endpoint":
      sizeInBytes += endpointSizeInBytes;
    }
    return sizeInBytes;
  }

  static void writeAnnotation(long timestamp, String value, @Nullable byte[] endpoint,
    WriteBuffer b) {
    b.writeAscii("{\"timestamp\":");
    b.writeAscii(timestamp);
    b.writeAscii(",\"value\":\"");
    b.writeUtf8(jsonEscape(value));
    b.writeByte('"');
    if (endpoint != null) {
      b.writeAscii(",\"endpoint\":");
      b.write(endpoint);
    }
    b.writeByte('}');
  }
}

public final class Span implements Serializable { // for Spark and Flink jobs
    static final Charset UTF_8 = Charset.forName("UTF-8");
    static final Endpoint EMPTY_ENDPOINT = Endpoint.newBuilder().build();

    static final int FLAG_DEBUG = 1 << 1;
    static final int FLAG_DEBUG_SET = 1 << 2;
    static final int FLAG_SHARED = 1 << 3;
    static final int FLAG_SHARED_SET = 1 << 4;

    private static final long serialVersionUID = 0L;

    /**
     * Trace identifier, set on all spans within it.
     *
     * <p>Encoded as 16 or 32 lowercase hex characters corresponding to 64 or 128 bits. For example,
     * a 128bit trace ID looks like {@code 4e441824ec2b6a44ffdc9bb9a6453df3}.
     *
     * <p>Some systems downgrade trace identifiers to 64bit by dropping the left-most 16 characters.
     * For example, {@code 4e441824ec2b6a44ffdc9bb9a6453df3} becomes {@code ffdc9bb9a6453df3}.
     */
    public String traceId() {
        return traceId;
    }

    /**
     * The parent's {@link #id} or null if this the root span in a trace.
     *
     * <p>This is the same encoding as {@link #id}. For example {@code ffdc9bb9a6453df3}
     */
    @Nullable
    public String parentId() {
        return parentId;
    }

    /**
     * Unique 64bit identifier for this operation within the trace.
     *
     * <p>Encoded as 16 lowercase hex characters. For example {@code ffdc9bb9a6453df3}
     *
     * <p>A span is uniquely identified in storage by ({@linkplain #traceId}, {@linkplain #id()}).
     */
    public String id() {
        return id;
    }

    /**
     * Indicates the primary span type.
     */
    public enum Kind {
        CLIENT,
        SERVER,
        /**
         * When present, {@link #timestamp()} is the moment a producer sent a message to a destination.
         * {@link #duration()} represents delay sending the message, such as batching, while {@link
         * #remoteEndpoint()} indicates the destination, such as a broker.
         *
         * <p>Unlike {@link #CLIENT}, messaging spans never share a span ID. For example, the {@link
         * #CONSUMER} of the same message has {@link #parentId()} set to this span's {@link #id()}.
         */
        PRODUCER,
        /**
         * When present, {@link #timestamp()} is the moment a consumer received a message from an
         * origin. {@link #duration()} represents delay consuming the message, such as from backlog,
         * while {@link #remoteEndpoint()} indicates the origin, such as a broker.
         *
         * <p>Unlike {@link #SERVER}, messaging spans never share a span ID. For example, the {@link
         * #PRODUCER} of this message is the {@link #parentId()} of this span.
         */
        CONSUMER
    }

    /**
     * When present, used to interpret {@link #remoteEndpoint}
     */
    @Nullable
    public Kind kind() {
        return kind;
    }

    /**
     * Span name in lowercase, rpc method for example.
     *
     * <p>Conventionally, when the span name isn't known, name = "unknown".
     */
    @Nullable
    public String name() {
        return name;
    }

    @Nullable
    public String belApp() {
        return belApp;
    }

    @Nullable
    public String belLogy() {
        return belLogy;
    }

    @Nullable
    public String belSer() {
        return belSer;
    }

    /**
     * Epoch microseconds of the start of this span, possibly absent if this an incomplete span.
     *
     * <p>This value should be set directly by instrumentation, using the most precise value
     * possible. For example, {@code gettimeofday} or multiplying {@link System#currentTimeMillis} by
     * 1000.
     *
     * <p>There are three known edge-cases where this could be reported absent:
     *
     * <pre><ul>
     * <li>A span was allocated but never started (ex not yet received a timestamp)</li>
     * <li>The span's start event was lost</li>
     * <li>Data about a completed span (ex tags) were sent after the fact</li>
     * </pre><ul>
     *
     * <p>Note: timestamps at or before epoch (0L == 1970) are invalid
     *
     * @see #duration()
     * @see #timestampAsLong()
     */
    @Nullable
    public Long timestamp() {
        return timestamp > 0 ? timestamp : null;
    }

    /**
     * Like {@link #timestamp()} except returns a primitive where zero implies absent.
     *
     * <p>Using this method will avoid allocation, so is encouraged when copying data.
     */
    public long timestampAsLong() {
        return timestamp;
    }

    /**
     * Measurement in microseconds of the critical path, if known. Durations of less than one
     * microsecond must be rounded up to 1 microsecond.
     *
     * <p>This value should be set directly, as opposed to implicitly via annotation timestamps.
     * Doing so encourages precision decoupled from problems of clocks, such as skew or NTP updates
     * causing time to move backwards.
     *
     * <p>If this field is persisted as unset, zipkin will continue to work, except duration query
     * support will be implementation-specific. Similarly, setting this field non-atomically is
     * implementation-specific.
     *
     * <p>This field is i64 vs i32 to support spans longer than 35 minutes.
     *
     * @see #durationAsLong()
     */
    @Nullable
    public Long duration() {
        return duration > 0 ? duration : null;
    }

    /**
     * Like {@link #duration()} except returns a primitive where zero implies absent.
     *
     * <p>Using this method will avoid allocation, so is encouraged when copying data.
     */
    public long durationAsLong() {
        return duration;
    }

    /**
     * The host that recorded this span, primarily for query by service name.
     *
     * <p>Instrumentation should always record this and be consistent as possible with the service
     * name as it is used in search. This is nullable for legacy reasons.
     */
    // Nullable for data conversion especially late arriving data which might not have an annotation
    @Nullable
    public Endpoint localEndpoint() {
        return localEndpoint;
    }

    /**
     * When an RPC (or messaging) span, indicates the other side of the connection.
     *
     * <p>By recording the remote endpoint, your trace will contain network context even if the peer
     * is not tracing. For example, you can record the IP from the {@code X-Forwarded-For} header or
     * the service name and socket of a remote peer.
     */
    @Nullable
    public Endpoint remoteEndpoint() {
        return remoteEndpoint;
    }

    /**
     * Events that explain latency with a timestamp. Unlike log statements, annotations are often
     * short or contain codes: for example "brave.flush". Annotations are sorted ascending by
     * timestamp.
     */
    public List<Annotation> annotations() {
        return annotations;
    }

    /**
     * Tags a span with context, usually to support query or aggregation.
     *
     * <p>For example, a tag key could be {@code "http.path"}.
     */
    public Map<String, String> tags() {
        return tags;
    }

    /**
     * True is a request to store this span even if it overrides sampling policy.
     */
    @Nullable
    public Boolean debug() {
        return (flags & FLAG_DEBUG_SET) == FLAG_DEBUG_SET
                ? (flags & FLAG_DEBUG) == FLAG_DEBUG
                : null;
    }

    /**
     * True if we are contributing to a span started by another tracer (ex on a different host).
     * Defaults to null. When set, it is expected for {@link #kind()} to be {@link Kind#SERVER}.
     *
     * <p>When an RPC trace is client-originated, it will be sampled and the same span ID is used for
     * the server side. However, the server shouldn't set span.timestamp or duration since it didn't
     * start the span.
     */
    @Nullable
    public Boolean shared() {
        return (flags & FLAG_SHARED_SET) == FLAG_SHARED_SET
                ? (flags & FLAG_SHARED) == FLAG_SHARED
                : null;
    }

    @Nullable
    public String localServiceName() {
        Endpoint localEndpoint = localEndpoint();
        return localEndpoint != null ? localEndpoint.serviceName() : null;
    }

    @Nullable
    public String remoteServiceName() {
        Endpoint remoteEndpoint = remoteEndpoint();
        return remoteEndpoint != null ? remoteEndpoint.serviceName() : null;
    }

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

    public Builder toBuilder() {
        return new Builder(this);
    }

    public static final class Builder {
        String traceId, parentId, id;
        String belApp, belSer, belLogy;
        Kind kind;
        String name;
        long timestamp, duration; // zero means null
        Endpoint localEndpoint, remoteEndpoint;
        ArrayList<Annotation> annotations;
        TreeMap<String, String> tags;
        int flags = 0; // bit field for timestamp and duration

        public Builder clear() {
            belApp = null;
            belLogy = null;
            belSer = null;
            traceId = null;
            parentId = null;
            id = null;
            kind = null;
            name = null;
            timestamp = 0L;
            duration = 0L;
            localEndpoint = null;
            remoteEndpoint = null;
            if (annotations != null) annotations.clear();
            if (tags != null) tags.clear();
            flags = 0;
            return this;
        }

        @Override
        public Builder clone() {
            Builder result = new Builder();
            result.traceId = traceId;
            result.parentId = parentId;
            result.id = id;
            result.kind = kind;
            result.name = name;
            result.belApp = belApp;
            result.belLogy = belLogy;
            result.belSer = belSer;
            result.timestamp = timestamp;
            result.duration = duration;
            result.localEndpoint = localEndpoint;
            result.remoteEndpoint = remoteEndpoint;
            if (annotations != null) {
                result.annotations = (ArrayList) annotations.clone();
            }
            if (tags != null) {
                result.tags = (TreeMap) tags.clone();
            }
            result.flags = flags;
            return result;
        }

        Builder(Span source) {
            traceId = source.traceId;
            parentId = source.parentId;
            id = source.id;
            kind = source.kind;
            name = source.name;
            belApp = source.belApp;
            belLogy = source.belLogy;
            belSer = source.belSer;
            timestamp = source.timestamp;
            duration = source.duration;
            localEndpoint = source.localEndpoint;
            remoteEndpoint = source.remoteEndpoint;
            if (!source.annotations.isEmpty()) {
                annotations = new ArrayList<>(source.annotations.size());
                annotations.addAll(source.annotations);
            }
            if (!source.tags.isEmpty()) {
                tags = new TreeMap<>();
                tags.putAll(source.tags);
            }
            flags = source.flags;
        }

        /**
         * Used to merge multiple incomplete spans representing the same operation on the same host. Do
         * not use this to merge spans that occur on different hosts.
         */
        public Builder merge(Span source) {
            if (traceId == null) traceId = source.traceId;
            if (id == null) id = source.id;
            if (parentId == null) parentId = source.parentId;
            if (kind == null) kind = source.kind;
            if (name == null) name = source.name;
            if (belSer == null) belSer = source.belSer;
            if (belLogy == null) belLogy = source.belLogy;
            if (belApp == null) belApp = source.belApp;
            if (timestamp == 0L) timestamp = source.timestamp;
            if (duration == 0L) duration = source.duration;
            if (localEndpoint == null) {
                localEndpoint = source.localEndpoint;
            } else if (source.localEndpoint != null) {
                localEndpoint = localEndpoint.toBuilder().merge(source.localEndpoint).build();
            }
            if (remoteEndpoint == null) {
                remoteEndpoint = source.remoteEndpoint;
            } else if (source.remoteEndpoint != null) {
                remoteEndpoint = remoteEndpoint.toBuilder().merge(source.remoteEndpoint).build();
            }
            if (!source.annotations.isEmpty()) {
                if (annotations == null) {
                    annotations = new ArrayList<>(source.annotations.size());
                }
                annotations.addAll(source.annotations);
            }
            if (!source.tags.isEmpty()) {
                if (tags == null) tags = new TreeMap<>();
                tags.putAll(source.tags);
            }
            flags = flags | source.flags;
            return this;
        }

        @Nullable
        public Kind kind() {
            return kind;
        }

        @Nullable
        public Endpoint localEndpoint() {
            return localEndpoint;
        }

        /**
         * Sets {@link Span#id()} or throws {@link IllegalArgumentException} if not lower-hex format.
         */
        public Builder traceId(String traceId) {
            this.traceId = normalizeTraceId(traceId);
            return this;
        }

        /**
         * Encodes 64 or 128 bits from the input into a hex trace ID.
         *
         * @param high Upper 64bits of the trace ID. Zero means the trace ID is 64-bit.
         * @param low  Lower 64bits of the trace ID.
         * @throws IllegalArgumentException if both values are zero
         */
        public Builder traceId(long high, long low) {
            if (high == 0L && low == 0L) throw new IllegalArgumentException("empty trace ID");
            char[] data = Platform.shortStringBuffer();
            int pos = 0;
            if (high != 0L) {
                writeHexLong(data, pos, high);
                pos += 16;
            }
            writeHexLong(data, pos, low);
            this.traceId = new String(data, 0, high != 0L ? 32 : 16);
            return this;
        }

        /**
         * Hex encodes the input as the {@link Span#parentId()} or unsets if the input is zero.
         */
        public Builder parentId(long parentId) {
            this.parentId = parentId != 0L ? toLowerHex(parentId) : null;
            return this;
        }

        /**
         * Sets {@link Span#parentId()} or throws {@link IllegalArgumentException} if not lower-hex
         * format.
         */
        public Builder parentId(@Nullable String parentId) {
            if (parentId == null) {
                this.parentId = null;
                return this;
            }
            int length = parentId.length();
            if (length == 0) throw new IllegalArgumentException("parentId is empty");
            if (length > 16) throw new IllegalArgumentException("parentId.length > 16");
            if (validateHexAndReturnZeroPrefix(parentId) == length) {
                this.parentId = null;
            } else {
                this.parentId = length < 16 ? padLeft(parentId, 16) : parentId;
            }
            return this;
        }

        /**
         * Hex encodes the input as the {@link Span#id()} or throws IllegalArgumentException if the
         * input is zero.
         */
        public Builder id(long id) {
            if (id == 0L) throw new IllegalArgumentException("empty id");
            this.id = toLowerHex(id);
            return this;
        }

        /**
         * Sets {@link Span#id()} or throws {@link IllegalArgumentException} if not lower-hex format.
         */
        public Builder id(String id) {
            if (id == null) throw new NullPointerException("id == null");
            int length = id.length();
            if (length == 0) throw new IllegalArgumentException("id is empty");
            if (length > 16) throw new IllegalArgumentException("id.length > 16");
            if (validateHexAndReturnZeroPrefix(id) == 16) {
                throw new IllegalArgumentException("id is all zeros");
            }
            this.id = length < 16 ? padLeft(id, 16) : id;
            return this;
        }

        /**
         * Sets {@link Span#kind}
         */
        public Builder kind(@Nullable Kind kind) {
            this.kind = kind;
            return this;
        }

        /**
         * Sets {@link Span#name}
         */
        public Builder name(@Nullable String name) {
            this.name = name == null || name.isEmpty() ? null : name.toLowerCase(Locale.ROOT);
            return this;
        }

        public Builder belApp(@Nullable String belApp) {
            this.belApp = belApp == null || belApp.isEmpty() ? null : belApp;
            return this;
        }

        public Builder belSer(@Nullable String belSer) {
            this.belSer = belSer == null || belSer.isEmpty() ? null : belSer;
            return this;
        }

        public Builder belLogy(@Nullable String belLogy) {
            this.belLogy = belLogy == null || belLogy.isEmpty() ? null : belLogy;
            return this;
        }

        /**
         * Sets {@link Span#timestampAsLong()}
         */
        public Builder timestamp(long timestamp) {
            if (timestamp < 0L) timestamp = 0L;
            this.timestamp = timestamp;
            return this;
        }

        /**
         * Sets {@link Span#timestamp()}
         */
        public Builder timestamp(@Nullable Long timestamp) {
            if (timestamp == null || timestamp < 0L) timestamp = 0L;
            this.timestamp = timestamp;
            return this;
        }

        /**
         * Sets {@link Span#durationAsLong()}
         */
        public Builder duration(long duration) {
            if (duration < 0L) duration = 0L;
            this.duration = duration;
            return this;
        }

        /**
         * Sets {@link Span#duration()}
         */
        public Builder duration(@Nullable Long duration) {
            if (duration == null || duration < 0L) duration = 0L;
            this.duration = duration;
            return this;
        }

        /**
         * Sets {@link Span#localEndpoint}
         */
        public Builder localEndpoint(@Nullable Endpoint localEndpoint) {
            if (EMPTY_ENDPOINT.equals(localEndpoint)) localEndpoint = null;
            this.localEndpoint = localEndpoint;
            return this;
        }

        /**
         * Sets {@link Span#remoteEndpoint}
         */
        public Builder remoteEndpoint(@Nullable Endpoint remoteEndpoint) {
            if (EMPTY_ENDPOINT.equals(remoteEndpoint)) remoteEndpoint = null;
            this.remoteEndpoint = remoteEndpoint;
            return this;
        }

        /**
         * Sets {@link Span#annotations}
         */
        public Builder addAnnotation(long timestamp, String value) {
            if (annotations == null) annotations = new ArrayList<>(2);
            annotations.add(Annotation.create(timestamp, value));
            return this;
        }

        /**
         * Sets {@link Span#annotations}
         */
        public Builder clearAnnotations() {
            if (annotations == null) return this;
            annotations.clear();
            return this;
        }

        /**
         * Sets {@link Span#tags}
         */
        public Builder putTag(String key, String value) {
            if (tags == null) tags = new TreeMap<>();
            if (key == null) throw new NullPointerException("key == null");
            if (value == null) throw new NullPointerException("value of " + key + " == null");
            this.tags.put(key, value);
            return this;
        }

        /**
         * Sets {@link Span#tags}
         */
        public Builder clearTags() {
            if (tags == null) return this;
            tags.clear();
            return this;
        }

        /**
         * Sets {@link Span#debug}
         */
        public Builder debug(boolean debug) {
            flags |= FLAG_DEBUG_SET;
            if (debug) {
                flags |= FLAG_DEBUG;
            } else {
                flags &= ~FLAG_DEBUG;
            }
            return this;
        }

        /**
         * Sets {@link Span#debug}
         */
        public Builder debug(@Nullable Boolean debug) {
            if (debug != null) return debug((boolean) debug);
            flags &= ~(FLAG_DEBUG_SET | FLAG_DEBUG);
            return this;
        }

        /**
         * Sets {@link Span#shared}
         */
        public Builder shared(boolean shared) {
            flags |= FLAG_SHARED_SET;
            if (shared) {
                flags |= FLAG_SHARED;
            } else {
                flags &= ~FLAG_SHARED;
            }
            return this;
        }

        /**
         * Sets {@link Span#shared}
         */
        public Builder shared(@Nullable Boolean shared) {
            if (shared != null) return shared((boolean) shared);
            flags &= ~(FLAG_SHARED_SET | FLAG_SHARED);
            return this;
        }

        public Span build() {
            String missing = "";
            if (traceId == null) missing += " traceId";
            if (id == null) missing += " id";
            if (!"".equals(missing)) throw new IllegalStateException("Missing :" + missing);
            if (id.equals(parentId)) { // edge case, so don't require a logger field
                Logger logger = Logger.getLogger(Span.class.getName());
                if (logger.isLoggable(FINEST)) {
                    logger.fine(format("undoing circular dependency: traceId=%s, spanId=%s", traceId, id));
                }
                parentId = null;
            }
            // shared is for the server side, unset it if accidentally set on the client side
            if ((flags & FLAG_SHARED) == FLAG_SHARED && kind == Kind.CLIENT) {
                Logger logger = Logger.getLogger(Span.class.getName());
                if (logger.isLoggable(FINEST)) {
                    logger.fine(format("removing shared flag on client: traceId=%s, spanId=%s", traceId, id));
                }
                shared(null);
            }
            return new Span(this);
        }

        Builder() {
        }
    }

    @Override
    public String toString() {
        return new String(SpanBytesEncoder.JSON_V2.encode(this), UTF_8);
    }

    /**
     * Returns a valid lower-hex trace ID, padded left as needed to 16 or 32 characters.
     *
     * @throws IllegalArgumentException if oversized or not lower-hex
     */
    public static String normalizeTraceId(String traceId) {
        if (traceId == null) throw new NullPointerException("traceId == null");
        int length = traceId.length();
        if (length == 0) throw new IllegalArgumentException("traceId is empty");
        if (length > 32) throw new IllegalArgumentException("traceId.length > 32");
        int zeros = validateHexAndReturnZeroPrefix(traceId);
        if (zeros == length) throw new IllegalArgumentException("traceId is all zeros");
        if (length == 32 || length == 16) {
            if (length == 32 && zeros >= 16) return traceId.substring(16);
            return traceId;
        } else if (length < 16) {
            return padLeft(traceId, 16);
        } else {
            return padLeft(traceId, 32);
        }
    }

    static final String THIRTY_TWO_ZEROS;

    static {
        char[] zeros = new char[32];
        Arrays.fill(zeros, '0');
        THIRTY_TWO_ZEROS = new String(zeros);
    }

    static String padLeft(String id, int desiredLength) {
        int length = id.length();
        int numZeros = desiredLength - length;

        char[] data = Platform.shortStringBuffer();
        THIRTY_TWO_ZEROS.getChars(0, numZeros, data, 0);
        id.getChars(0, length, data, numZeros);

        return new String(data, 0, desiredLength);
    }

    static String toLowerHex(long v) {
        char[] data = Platform.shortStringBuffer();
        writeHexLong(data, 0, v);
        return new String(data, 0, 16);
    }

    /**
     * Inspired by {@code okio.Buffer.writeLong}
     */
    static void writeHexLong(char[] data, int pos, long v) {
        writeHexByte(data, pos + 0, (byte) ((v >>> 56L) & 0xff));
        writeHexByte(data, pos + 2, (byte) ((v >>> 48L) & 0xff));
        writeHexByte(data, pos + 4, (byte) ((v >>> 40L) & 0xff));
        writeHexByte(data, pos + 6, (byte) ((v >>> 32L) & 0xff));
        writeHexByte(data, pos + 8, (byte) ((v >>> 24L) & 0xff));
        writeHexByte(data, pos + 10, (byte) ((v >>> 16L) & 0xff));
        writeHexByte(data, pos + 12, (byte) ((v >>> 8L) & 0xff));
        writeHexByte(data, pos + 14, (byte) (v & 0xff));
    }

    static void writeHexByte(char[] data, int pos, byte b) {
        data[pos + 0] = HEX_DIGITS[(b >> 4) & 0xf];
        data[pos + 1] = HEX_DIGITS[b & 0xf];
    }

    static int validateHexAndReturnZeroPrefix(String id) {
        int zeros = 0;
        boolean inZeroPrefix = id.charAt(0) == '0';
        for (int i = 0, length = id.length(); i < length; i++) {
            char c = id.charAt(i);
            if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) {
                throw new IllegalArgumentException(id + " should be lower-hex encoded with no prefix");
            }
            if (c != '0') {
                inZeroPrefix = false;
            } else if (inZeroPrefix) {
                zeros++;
            }
        }
        return zeros;
    }

    static <T extends Comparable<? super T>> List<T> sortedList(@Nullable List<T> in) {
        if (in == null || in.isEmpty()) return Collections.emptyList();
        if (in.size() == 1) return Collections.singletonList(in.get(0));
        Object[] array = in.toArray();
        Arrays.sort(array);

        // dedupe
        int j = 0, i = 1;
        while (i < array.length) {
            if (!array[i].equals(array[j])) {
                array[++j] = array[i];
            }
            i++;
        }

        List result = Arrays.asList(i == j + 1 ? array : Arrays.copyOf(array, j + 1));
        return Collections.unmodifiableList(result);
    }

    // Custom impl to reduce GC churn and Kryo which cannot handle AutoValue subclass
    // See https://github.com/openzipkin/zipkin/issues/1879
    final String traceId, parentId, id;
    final Kind kind;
    final String name;
    final long timestamp, duration; // zero means null, saving 2 object references
    final Endpoint localEndpoint, remoteEndpoint;
    final List<Annotation> annotations;
    final Map<String, String> tags;
    final int flags; // bit field for timestamp and duration, saving 2 object references
    String belApp;
    String belLogy;
    String belSer;

    Span(Builder builder) {
        traceId = builder.traceId;
        // prevent self-referencing spans
        parentId = builder.id.equals(builder.parentId) ? null : builder.parentId;
        id = builder.id;
        kind = builder.kind;
        name = builder.name;
        belApp = builder.belApp;
        belLogy = builder.belLogy;
        belSer = builder.belSer;
        timestamp = builder.timestamp;
        duration = builder.duration;
        localEndpoint = builder.localEndpoint;
        remoteEndpoint = builder.remoteEndpoint;
        annotations = sortedList(builder.annotations);
        tags = builder.tags == null ? Collections.emptyMap() : new LinkedHashMap<>(builder.tags);
        flags = builder.flags;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Span)) return false;
        Span that = (Span) o;
        return traceId.equals(that.traceId)
                && (parentId == null ? that.parentId == null : parentId.equals(that.parentId))
                && id.equals(that.id)
                && (kind == null ? that.kind == null : kind.equals(that.kind))
                && (name == null ? that.name == null : name.equals(that.name))
                && timestamp == that.timestamp
                && duration == that.duration
                && (localEndpoint == null
                ? that.localEndpoint == null : localEndpoint.equals(that.localEndpoint))
                && (remoteEndpoint == null
                ? that.remoteEndpoint == null : remoteEndpoint.equals(that.remoteEndpoint))
                && annotations.equals(that.annotations)
                && tags.equals(that.tags)
                && flags == that.flags;
    }

    @Override
    public int hashCode() {
        int h = 1;
        h *= 1000003;
        h ^= traceId.hashCode();
        h *= 1000003;
        h ^= (parentId == null) ? 0 : parentId.hashCode();
        h *= 1000003;
        h ^= id.hashCode();
        h *= 1000003;
        h ^= (kind == null) ? 0 : kind.hashCode();
        h *= 1000003;
        h ^= (name == null) ? 0 : name.hashCode();
        h *= 1000003;
        h ^= (int) (h ^ ((timestamp >>> 32) ^ timestamp));
        h *= 1000003;
        h ^= (int) (h ^ ((duration >>> 32) ^ duration));
        h *= 1000003;
        h ^= (localEndpoint == null) ? 0 : localEndpoint.hashCode();
        h *= 1000003;
        h ^= (remoteEndpoint == null) ? 0 : remoteEndpoint.hashCode();
        h *= 1000003;
        h ^= annotations.hashCode();
        h *= 1000003;
        h ^= tags.hashCode();
        h *= 1000003;
        h ^= flags;
        return h;
    }

    // This is an immutable object, and our encoder is faster than java's: use a serialization proxy.
    final Object writeReplace() throws ObjectStreamException {
        return new SerializedForm(SpanBytesEncoder.PROTO3.encode(this));
    }

    private static final class SerializedForm implements Serializable {
        private static final long serialVersionUID = 0L;

        final byte[] bytes;

        SerializedForm(byte[] bytes) {
            this.bytes = bytes;
        }

        Object readResolve() throws ObjectStreamException {
            try {
                return SpanBytesDecoder.PROTO3.decodeOne(bytes);
            } catch (IllegalArgumentException e) {
                throw new StreamCorruptedException(e.getMessage());
            }
        }
    }
}

package zipkin2.elasticsearch;

/** Returns version-specific index templates */
// TODO: make a main class that spits out the index template using ENV variables for the server,
// a parameter for the version, and a parameter for the index type. Ex.
// java -cp zipkin-storage-elasticsearch.jar zipkin2.elasticsearch.VersionSpecificTemplates 6.7 span
final class VersionSpecificTemplates {
  /** Maximum character length constraint of most names, IP literals and IDs. */
  static final int SHORT_STRING_LENGTH = 256;
  static final String TYPE_AUTOCOMPLETE = "autocomplete";
  static final String TYPE_SPAN = "span";
  static final String TYPE_DEPENDENCY = "dependency";

  /**
   * In Zipkin search, we do exact match only (keyword). Norms is about scoring. We don't use that
   * in our API, and disable it to reduce disk storage needed.
   */
  static final String KEYWORD = "{ \"type\": \"keyword\", \"norms\": false }";

  final String indexPrefix;
  final int indexReplicas, indexShards;
  final boolean searchEnabled, strictTraceId;

  VersionSpecificTemplates(String indexPrefix, int indexReplicas, int indexShards,
    boolean searchEnabled, boolean strictTraceId) {
    this.indexPrefix = indexPrefix;
    this.indexReplicas = indexReplicas;
    this.indexShards = indexShards;
    this.searchEnabled = searchEnabled;
    this.strictTraceId = strictTraceId;
  }

  String indexPattern(String type, float version) {
    return '"'
      + (version < 6.0f ? "template" : "index_patterns")
      + "\": \""
      + indexPrefix
      + indexTypeDelimiter(version)
      + type
      + "-*"
      + "\"";
  }

  String indexProperties(float version) {
    // 6.x _all disabled https://www.elastic.co/guide/en/elasticsearch/reference/6.7/breaking-changes-6.0.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default
    // 7.x _default disallowed https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#_the_literal__default__literal_mapping_is_no_longer_allowed
    String result = ""
      + "    \"index.number_of_shards\": " + indexShards + ",\n"
      + "    \"index.number_of_replicas\": " + indexReplicas + ",\n"
      + "    \"index.requests.cache.enable\": true";
    // There is no explicit documentation of index.mapper.dynamic being removed in v7, but it was.
    if (version >= 7.0f) return result + "\n";
    return result + ",\n    \"index.mapper.dynamic\": false\n";
  }

  /** Templatized due to version differences. Only fields used in search are declared */
  String spanIndexTemplate(float version) {
    String result = "{\n"
      + "  " + indexPattern(TYPE_SPAN, version) + ",\n"
      + "  \"settings\": {\n"
      + indexProperties(version);

    String traceIdMapping = KEYWORD;
    if (!strictTraceId) {
      // Supporting mixed trace ID length is expensive due to needing a special analyzer and
      // "fielddata" which consumes a lot of heap. Sites should only turn off strict trace ID when
      // in a transition, and keep trace ID length transitions as short time as possible.
      traceIdMapping =
        "{ \"type\": \"text\", \"fielddata\": \"true\", \"analyzer\": \"traceId_analyzer\" }";
      result += (",\n"
        + "    \"analysis\": {\n"
        + "      \"analyzer\": {\n"
        + "        \"traceId_analyzer\": {\n"
        + "          \"type\": \"custom\",\n"
        + "          \"tokenizer\": \"keyword\",\n"
        + "          \"filter\": \"traceId_filter\"\n"
        + "        }\n"
        + "      },\n"
        + "      \"filter\": {\n"
        + "        \"traceId_filter\": {\n"
        + "          \"type\": \"pattern_capture\",\n"
        + "          \"patterns\": [\"([0-9a-f]{1,16})$\"],\n"
        + "          \"preserve_original\": true\n"
        + "        }\n"
        + "      }\n"
        + "    }\n");
    }

    result += "  },\n";

    if (searchEnabled) {
      return result
        + ("  \"mappings\": {\n"
        + maybeWrap(TYPE_SPAN, version, ""
        + "    \"_source\": {\"excludes\": [\"_q\"] },\n"
        + "    \"dynamic_templates\": [\n"
        + "      {\n"
        + "        \"strings\": {\n"
        + "          \"mapping\": {\n"
        + "            \"type\": \"keyword\",\"norms\": false,"
        + " \"ignore_above\": " + SHORT_STRING_LENGTH + "\n"
        + "          },\n"
        + "          \"match_mapping_type\": \"string\",\n"
        + "          \"match\": \"*\"\n"
        + "        }\n"
        + "      }\n"
        + "    ],\n"
        + "    \"properties\": {\n"
        + "      \"traceId\": " + traceIdMapping + ",\n"
        + "      \"belSer\": " + KEYWORD + ",\n"
        + "      \"belApp\": " + KEYWORD + ",\n"
        + "      \"belLogy\": " + KEYWORD + ",\n"
        + "      \"name\": " + KEYWORD + ",\n"
        + "      \"localEndpoint\": {\n"
        + "        \"type\": \"object\",\n"
        + "        \"dynamic\": false,\n"
        + "        \"properties\": { \"serviceName\": " + KEYWORD + " }\n"
        + "      },\n"
        + "      \"remoteEndpoint\": {\n"
        + "        \"type\": \"object\",\n"
        + "        \"dynamic\": false,\n"
        + "        \"properties\": { \"serviceName\": " + KEYWORD + " }\n"
        + "      },\n"
        + "      \"timestamp_millis\": {\n"
        + "        \"type\":   \"date\",\n"
        + "        \"format\": \"epoch_millis\"\n"
        + "      },\n"
        + "      \"duration\": { \"type\": \"long\" },\n"
        + "      \"annotations\": { \"enabled\": false },\n"
        + "      \"tags\": {\n"
        + "        \"type\": \"object\",\n"
        + "        \"dynamic\": false,\n"
        + "        \"properties\": {\n"
        + "          \"belApp\": " + KEYWORD + ",\n"
        + "          \"belLogy\": " + KEYWORD + ",\n"
        + "          \"belSer\": " + KEYWORD + "\n"
        + "        }\n"
        + "      },\n"
        + "      \"_q\": " + KEYWORD + "\n"
        + "    }\n")
        + "  }\n"
        + "}");
    }
    return result
      + ("  \"mappings\": {\n"
      + maybeWrap(TYPE_SPAN, version, ""
      + "    \"properties\": {\n"
      + "      \"traceId\": " + KEYWORD + ",\n"
      + "      \"belSer\": " + KEYWORD + ",\n"
      + "      \"belApp\": " + KEYWORD + ",\n"
      + "      \"belLogy\": " + KEYWORD + ",\n"
      + "      \"annotations\": { \"enabled\": false },\n"
      + "      \"tags\": {\n"
      + "        \"type\": \"object\",\n"
      + "        \"dynamic\": false,\n"
      + "        \"properties\": {\n"
      + "          \"belApp\": " + KEYWORD + ",\n"
      + "          \"belLogy\": " + KEYWORD + ",\n"
      + "          \"belSer\": " + KEYWORD + "\n"
      + "        }\n"
      + "      },\n"
      + "    }\n")
      + "  }\n"
      + "}");
  }

  /** Templatized due to version differences. Only fields used in search are declared */
  String dependencyTemplate(float version) {
    return "{\n"
      + "  " + indexPattern(TYPE_DEPENDENCY, version) + ",\n"
      + "  \"settings\": {\n"
      + indexProperties(version)
      + "  },\n"
      + "  \"mappings\": {\n"
      + maybeWrap(TYPE_DEPENDENCY, version, "    \"enabled\": false\n")
      + "  }\n"
      + "}";
  }

  // The key filed of a autocompleteKeys is intentionally names as tagKey since it clashes with the
  // BodyConverters KEY
  String autocompleteTemplate(float version) {
    return "{\n"
      + "  " + indexPattern(TYPE_AUTOCOMPLETE, version) + ",\n"
      + "  \"settings\": {\n"
      + indexProperties(version)
      + "  },\n"
      + "  \"mappings\": {\n"
      + maybeWrap(TYPE_AUTOCOMPLETE, version, ""
      + "    \"enabled\": true,\n"
      + "    \"properties\": {\n"
      + "      \"tagKey\": " + KEYWORD + ",\n"
      + "      \"tagValue\": " + KEYWORD + "\n"
      + "    }\n")
      + "  }\n"
      + "}";
  }

  IndexTemplates get(float version) {
    if (version < 5.0f || version >= 8.0f) {
      throw new IllegalArgumentException(
        "Elasticsearch versions 5-7.x are supported, was: " + version);
    }
    return IndexTemplates.newBuilder()
      .version(version)
      .indexTypeDelimiter(indexTypeDelimiter(version))
      .span(spanIndexTemplate(version))
      .dependency(dependencyTemplate(version))
      .autocomplete(autocompleteTemplate(version))
      .build();
  }

  /**
   * This returns a delimiter based on what's supported by the Elasticsearch version.
   *
   * <p>Starting in Elasticsearch 7.x, colons are no longer allowed in index names. This logic will
   * make sure the pattern in our index template doesn't use them either.
   *
   * <p>See https://github.com/openzipkin/zipkin/issues/2219
   */
  static char indexTypeDelimiter(float version) {
    return version < 7.0f ? ':' : '-';
  }

  static String maybeWrap(String type, float version, String json) {
    // ES 7.x defaults include_type_name to false https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#_literal_include_type_name_literal_now_defaults_to_literal_false_literal
    if (version >= 7.0f) return json;
    return "    \"" + type + "\": {\n  " + json.replace("\n", "\n  ") + "  }\n";
  }
}


zipkin2.internal.Platform.SHORT_STRING_LENGTH;

public abstract class BulkIndexWriter<T> {

    /**
     * Write a complete json document according to index strategy and returns the ID field.
     */
    public abstract String writeDocument(T input, ByteBufOutputStream sink);

    public static final BulkIndexWriter<Span> SPAN = new BulkIndexWriter<Span>() {
        @Override
        public String writeDocument(Span input, ByteBufOutputStream sink) {
            return write(input, true, sink);
        }
    };
    public static final BulkIndexWriter<Span>
            SPAN_SEARCH_DISABLED = new BulkIndexWriter<Span>() {
        @Override
        public String writeDocument(Span input, ByteBufOutputStream sink) {
            return write(input, false, sink);
        }
    };

    public static final BulkIndexWriter<Map.Entry<String, String>> AUTOCOMPLETE =
            new BulkIndexWriter<Map.Entry<String, String>>() {
                @Override
                public String writeDocument(Map.Entry<String, String> input,
                                            ByteBufOutputStream sink) {
                    try (JsonGenerator writer = JsonSerializers.jsonGenerator(sink)) {
                        writeAutocompleteEntry(input.getKey(), input.getValue(), writer);
                    } catch (IOException e) {
                        throw new AssertionError("Couldn't close generator for a memory stream.", e);
                    }
                    // Id is used to dedupe server side as necessary. Arbitrarily same format as _q value.
                    return input.getKey() + '=' + input.getValue();
                }
            };

    static final Endpoint EMPTY_ENDPOINT = Endpoint.newBuilder().build();

    /**
     * In order to allow systems like Kibana to search by timestamp, we add a field "timestamp_millis"
     * when storing. The cheapest way to do this without changing the codec is prefixing it to the
     * json. For example. {"traceId":"... becomes {"timestamp_millis":12345,"traceId":"...
     *
     * <p>Tags are stored as a dictionary. Since some tag names will include inconsistent number of
     * dots (ex "error" and perhaps "error.message"), we cannot index them naturally with
     * elasticsearch. Instead, we add an index-only (non-source) field of {@code _q} which includes
     * valid search queries. For example, the tag {@code error -> 500} results in {@code
     * "_q":["error", "error=500"]}. This matches the input query syntax, and can be checked manually
     * with curl.
     *
     * <p>Ex {@code curl -s localhost:9200/zipkin:span-2017-08-11/_search?q=_q:error=500}
     *
     * @param searchEnabled encodes timestamp_millis and _q when non-empty
     */
    static String write(Span span, boolean searchEnabled, ByteBufOutputStream sink) {
        int startIndex = sink.buffer().writerIndex();
        String belApp = "";
        String belSer = "";
        String belLogy = "";
        try (JsonGenerator writer = JsonSerializers.jsonGenerator(sink)) {
            writer.writeStartObject();
            if (searchEnabled) addSearchFields(span, writer);
            writer.writeStringField("traceId", span.traceId());
            if (span.parentId() != null) writer.writeStringField("parentId", span.parentId());
            writer.writeStringField("id", span.id());
            if (span.kind() != null) writer.writeStringField("kind", span.kind().toString());
            if (span.name() != null) writer.writeStringField("name", span.name());

            if (span.timestampAsLong() != 0L) {
                writer.writeNumberField("timestamp", span.timestampAsLong());
            }
            if (span.durationAsLong() != 0L) writer.writeNumberField("duration", span.durationAsLong());
            if (span.localEndpoint() != null && !EMPTY_ENDPOINT.equals(span.localEndpoint())) {
                writer.writeFieldName("localEndpoint");
                write(span.localEndpoint(), writer);
            }
            if (span.remoteEndpoint() != null && !EMPTY_ENDPOINT.equals(span.remoteEndpoint())) {
                writer.writeFieldName("remoteEndpoint");
                write(span.remoteEndpoint(), writer);
            }
            if (!span.annotations().isEmpty()) {
                writer.writeArrayFieldStart("annotations");
                for (int i = 0, length = span.annotations().size(); i < length; ) {
                    write(span.annotations().get(i++), writer);
                }
                writer.writeEndArray();
            }
            if (!span.tags().isEmpty()) {
                writer.writeObjectFieldStart("tags");
                Iterator<Map.Entry<String, String>> tags = span.tags().entrySet().iterator();
                while (tags.hasNext()) {
                    Map.Entry<String, String> next = tags.next();
                    write(next, writer);
                    String nextKey = next.getKey();
                    String value = next.getValue();
                    // 自定义数据写入
                    if ("belApp".equalsIgnoreCase(nextKey)) {
                        belApp = value;
                    }
                    if ("belLogy".equalsIgnoreCase(nextKey)) {
                        belLogy = value;
                    }
                    if ("belSer".equalsIgnoreCase(nextKey)) {
                        belSer = value;
                    }
                }
                writer.writeEndObject();
            }
            // 自定义数据写入
            if (!StringUtil.isNullOrEmpty(belApp)) writer.writeStringField("belApp", belApp);
            if (!StringUtil.isNullOrEmpty(belLogy)) writer.writeStringField("belLogy", belLogy);
            if (!StringUtil.isNullOrEmpty(belSer)) writer.writeStringField("belSer", belSer);

            if (Boolean.TRUE.equals(span.debug())) writer.writeBooleanField("debug", true);
            if (Boolean.TRUE.equals(span.shared())) writer.writeBooleanField("shared", true);
            writer.writeEndObject();
        } catch (IOException e) {
            throw new AssertionError(e); // No I/O writing to a Buffer.
        }

        // get a slice representing the document we just wrote so that we can make a content hash
        ByteBuf slice = sink.buffer().slice(startIndex, sink.buffer().writerIndex() - startIndex);

        return span.traceId() + '-' + md5(slice);
    }

    static void writeAutocompleteEntry(String key, String value, JsonGenerator writer) {
        try {
            writer.writeStartObject();
            writer.writeStringField("tagKey", key);
            writer.writeStringField("tagValue", value);
            writer.writeEndObject();
        } catch (IOException e) {
            throw new AssertionError(e); // No I/O writing to a Buffer.
        }
    }

    static void write(Map.Entry<String, String> tag, JsonGenerator writer) throws IOException {
        writer.writeStringField(tag.getKey(), tag.getValue());
    }

    static void write(Annotation annotation, JsonGenerator writer) throws IOException {
        writer.writeStartObject();
        writer.writeNumberField("timestamp", annotation.timestamp());
        writer.writeStringField("value", annotation.value());
        writer.writeEndObject();
    }

    static void write(Endpoint endpoint, JsonGenerator writer) throws IOException {
        writer.writeStartObject();
        if (endpoint.serviceName() != null) {
            writer.writeStringField("serviceName", endpoint.serviceName());
        }
        if (endpoint.ipv4() != null) writer.writeStringField("ipv4", endpoint.ipv4());
        if (endpoint.ipv6() != null) writer.writeStringField("ipv6", endpoint.ipv6());
        if (endpoint.portAsInt() != 0) writer.writeNumberField("port", endpoint.portAsInt());
        writer.writeEndObject();
    }

    static void addSearchFields(Span span, JsonGenerator writer) throws IOException {
        long timestampMillis = span.timestampAsLong() / 1000L;
        if (timestampMillis != 0L) writer.writeNumberField("timestamp_millis", timestampMillis);
        if (!span.tags().isEmpty() || !span.annotations().isEmpty()) {
            writer.writeArrayFieldStart("_q");
            for (Annotation a : span.annotations()) {
                if (a.value().length() > SHORT_STRING_LENGTH) continue;
                writer.writeString(a.value());
            }
            for (Map.Entry<String, String> tag : span.tags().entrySet()) {
                int length = tag.getKey().length() + tag.getValue().length() + 1;
                if (length > SHORT_STRING_LENGTH) continue;
                writer.writeString(tag.getKey()); // search is possible by key alone
                writer.writeString(tag.getKey() + "=" + tag.getValue());
            }
            writer.writeEndArray();
        }
    }

    static String md5(ByteBuf buf) {
        final MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError();
        }
        messageDigest.update(buf.nioBuffer());
        return ByteBufUtil.hexDump(messageDigest.digest());
    }
}

从es的索引中添加的自定义字段生效

{
    "zipkin-span-2023-03-23": {
        "mappings": {
            "_source": {
                "excludes": [
                    "_q"
                ]
            },
            "dynamic_templates": [
                {
                    "strings": {
                        "match": "*",
                        "match_mapping_type": "string",
                        "mapping": {
                            "ignore_above": 256,
                            "norms": false,
                            "type": "keyword"
                        }
                    }
                }
            ],
            "properties": {
                "_q": {
                    "type": "keyword"
                },
                "annotations": {
                    "type": "object",
                    "enabled": false
                },
                "belApp": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "belLogy": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "belSer": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "duration": {
                    "type": "long"
                },
                "id": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "kind": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "localEndpoint": {
                    "dynamic": "false",
                    "properties": {
                        "serviceName": {
                            "type": "keyword"
                        }
                    }
                },
                "name": {
                    "type": "keyword"
                },
                "parentId": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "remoteEndpoint": {
                    "dynamic": "false",
                    "properties": {
                        "serviceName": {
                            "type": "keyword"
                        }
                    }
                },
                "shared": {
                    "type": "boolean"
                },
                "tags": {
                    "type": "object",
                    "enabled": false
                },
                "timestamp": {
                    "type": "long"
                },
                "timestamp_millis": {
                    "type": "date",
                    "format": "epoch_millis"
                },
                "traceId": {
                    "type": "keyword"
                }
            }
        }
    }
}
```![在这里插入图片描述](https://img-blog.csdnimg.cn/eced55dfa1cb4bc5b7ee2e5973cfc764.png)


![在这里插入图片描述](https://img-blog.csdnimg.cn/1d1e51f81e61422e9fcd966a195e164d.png)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值