场景:项目中通过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"
}
}
}
}
}
```
