package org.stagemonitor.tracing.reporter; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleSerializers; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import org.stagemonitor.core.util.JsonUtils; import org.stagemonitor.util.StringUtils; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import io.opentracing.tag.Tags; /** * A span which supports readback of tags and meta data */ public class ReadbackSpan { static { JsonUtils.getMapper().registerModule(new SpanJsonModule()); } private String id; private String traceId; private String parentId; private String name; private double duration; private String timestamp; private Map<String, Object> tags = new HashMap<String, Object>(); public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getParentId() { return parentId; } public void setParentId(String parentId) { this.parentId = parentId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getDuration() { return duration; } public void setDuration(double duration) { this.duration = duration; } public String getTimestamp() { return timestamp; } public void setTimestamp(String timestamp) { this.timestamp = timestamp; } public Map<String, Object> getTags() { return tags; } public void setTag(String key, Object value) { if (value != null) { synchronized (this) { tags.put(key, value); } } } public static class SpanJsonModule extends Module { @Override public String getModuleName() { return "stagemonitor-spans"; } @Override public Version version() { return new Version(1, 0, 0, "", "org.stagemonitor", "stagemonitor-requestmonitor"); } @Override public void setupModule(final SetupContext context) { context.addSerializers(new SimpleSerializers(Collections.<JsonSerializer<?>>singletonList(new StdSerializer<ReadbackSpan>(ReadbackSpan.class) { @Override public void serialize(ReadbackSpan span, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartObject(); final Map<String, Object> nestedTags; // synchronizing on the span avoids ConcurrentModificationExceptions // in case other threads are for example adding tags while the span is converted to JSON synchronized (span) { nestedTags = convertDottedKeysIntoNestedObject(span.getTags()); } for (Map.Entry<String, Object> entry : nestedTags.entrySet()) { final Object value = entry.getValue(); if (value != null) { gen.writeObjectField(entry.getKey(), value); } } // always include error tag so we can have a successful/error filter in Kibana if (!nestedTags.containsKey(Tags.ERROR.getKey())) { gen.writeBooleanField("error", false); } gen.writeStringField("name", span.getName()); gen.writeNumberField("duration_ms", span.getDuration()); gen.writeStringField("@timestamp", span.getTimestamp()); gen.writeStringField("id", span.getId()); gen.writeStringField("trace_id", span.getTraceId()); gen.writeStringField("parent_id", span.getParentId()); gen.writeEndObject(); } }))); } /** * As Elasticsearch can't cope with dots in field names, we have to convert them into nested objects. * That way, we can use the dotted notation in aggregations again. * * Example: * <pre> * { * "dotted.path.foo": "bar" * } * * will be converted to * * { * "dotted": { * "path": { * "foo": "bar" * } * } * } * </pre> * * @param tags the span tags to convert * @return the span tags as nested objects */ private Map<String, Object> convertDottedKeysIntoNestedObject(Map<String, Object> tags) { Map<String, Object> nestedTags = new HashMap<String, Object>(); for (Map.Entry<String, Object> entry : tags.entrySet()) { if (entry.getKey().indexOf('.') >= 0) { doConvertDots(nestedTags, entry.getKey(), entry.getValue()); } else { nestedTags.put(entry.getKey(), entry.getValue()); } } return nestedTags; } private void doConvertDots(Map<String, Object> nestedTags, String key, Object value) { final String[] pathSegments = StringUtils.split(key, '.'); Map<String, Object> path = nestedTags; for (int i = 0; i < pathSegments.length; i++) { String pathSegment = pathSegments[i]; if (i + 1 < pathSegments.length) { // starting.segments path = getNewNestedPath(path, pathSegment, key); } else { // last.segment path.put(pathSegment, value); } } } private Map<String, Object> getNewNestedPath(Map<String, Object> path, String pathSegment, String fullPath) { final Map<String, Object> newPath; final Object existingPath = path.get(pathSegment); if (existingPath != null) { if (existingPath instanceof Map) { newPath = (Map<String, Object>) existingPath; } else { throw new IllegalArgumentException("Ambiguous mapping for " + fullPath); } } else { newPath = new LinkedHashMap<String, Object>(); path.put(pathSegment, newPath); } return newPath; } } }