Skip to content

Commit 3c6fcd4

Browse files
authored
Merge pull request #178 from ExpediaDotCom/server-client-merge-transform
merge client and server spans that cross service boundaries
2 parents 7fd84fd + a0ee88e commit 3c6fcd4

File tree

6 files changed

+342
-104
lines changed

6 files changed

+342
-104
lines changed

reader/src/main/resources/config/base.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ trace {
7575
sequence = [
7676
# "com.expedia.www.haystack.trace.reader.readers.transformers.OrphanedTraceTransformer"
7777
"com.expedia.www.haystack.trace.reader.readers.transformers.PartialSpanTransformer"
78+
"com.expedia.www.haystack.trace.reader.readers.transformers.ServerClientSpanMergeTransformer"
7879
"com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewTransformer"
79-
"com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewFromParentTransformer"
8080
"com.expedia.www.haystack.trace.reader.readers.transformers.SortSpanTransformer"
8181
]
8282
}

reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PartialSpanTransformer.scala

Lines changed: 4 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,8 @@
1616

1717
package com.expedia.www.haystack.trace.reader.readers.transformers
1818

19-
import com.expedia.open.tracing.{Span, Tag}
20-
import com.expedia.www.haystack.trace.reader.readers.utils.TagBuilders.{buildBoolTag, buildLongTag, buildStringTag}
21-
import com.expedia.www.haystack.trace.reader.readers.utils.TagExtractors.extractTagStringValue
22-
import com.expedia.www.haystack.trace.reader.readers.utils.{AuxiliaryTags, MutableSpanForest, SpanMarkers, SpanUtils}
23-
24-
import scala.collection.JavaConverters._
19+
import com.expedia.open.tracing.Span
20+
import com.expedia.www.haystack.trace.reader.readers.utils._
2521

2622
/**
2723
* Merges partial spans and generates a single Span combining a client and corresponding server span
@@ -33,106 +29,11 @@ class PartialSpanTransformer extends SpanTreeTransformer {
3329

3430
val mergedSpans: Seq[Span] = spanForest.getUnderlyingSpans.groupBy(_.getSpanId).map((pair) => pair._2 match {
3531
case Seq(span: Span) => span
36-
case list: Seq[Span] =>
32+
case spans: Seq[Span] =>
3733
hasAnySpanMerged = true
38-
mergeSpans(list)
34+
SpanMerger.mergeSpans(spans)
3935
}).toSeq
4036

4137
spanForest.updateUnderlyingSpans(mergedSpans, hasAnySpanMerged)
4238
}
43-
44-
private def mergeSpans(spans: Seq[Span]): Span = {
45-
val serverSpanOptional = collapseSpans(spans.filter(SpanUtils.containsServerLogTag))
46-
val clientSpanOptional = collapseSpans(spans.filter(SpanUtils.containsClientLogTag))
47-
48-
(clientSpanOptional, serverSpanOptional) match {
49-
// ideally there should be one server and one client span
50-
// merging these partial spans to form a new single span
51-
case (Some(clientSpan), Some(serverSpan)) =>
52-
Span
53-
.newBuilder(serverSpan)
54-
.setParentSpanId(clientSpan.getParentSpanId) // use the parentSpanId of the client span to stitch in the client's trace tree
55-
.addAllTags((clientSpan.getTagsList.asScala ++ auxiliaryCommonTags(clientSpan, serverSpan) ++ auxiliaryClientTags(clientSpan) ++ auxiliaryServerTags(serverSpan)).asJavaCollection)
56-
.clearLogs().addAllLogs((clientSpan.getLogsList.asScala ++ serverSpan.getLogsList.asScala.sortBy(_.getTimestamp)).asJavaCollection)
57-
.build()
58-
59-
// imperfect scenario, fallback to return available server span
60-
case (None, Some(serverSpan)) => serverSpan
61-
62-
// imperfect scenario, fallback to return available client span
63-
case (Some(clientSpan), None) => clientSpan
64-
65-
// imperfect scenario, fallback to collapse all spans
66-
case _ => collapseSpans(spans).get
67-
}
68-
}
69-
70-
// collapse all spans of a type(eg. client or server) if needed,
71-
// ideally there would be just one span in the list and hence no need of collapsing
72-
private def collapseSpans(spans: Seq[Span]): Option[Span] = {
73-
spans match {
74-
case Nil => None
75-
case Seq(span) => Some(span)
76-
case _ =>
77-
// if there are multiple spans fallback to collapse all the spans in a single span
78-
// start the collapsed span from startTime of the first and end at ending of last such span
79-
// also add an error marker in the collapsed span
80-
val firstSpan = spans.minBy(_.getStartTime)
81-
val lastSpan = spans.maxBy(span => span.getStartTime + span.getDuration)
82-
val allTags = spans.flatMap(span => span.getTagsList.asScala)
83-
val allLogs = spans.flatMap(span => span.getLogsList.asScala)
84-
val opName = spans.map(_.getOperationName).reduce((a, b) => a + " & " + b)
85-
86-
Some(
87-
Span
88-
.newBuilder(firstSpan)
89-
.setOperationName(opName)
90-
.setDuration(lastSpan.getStartTime + lastSpan.getDuration - firstSpan.getStartTime)
91-
.clearTags().addAllTags(allTags.asJava)
92-
.addTags(buildBoolTag(AuxiliaryTags.ERR_IS_MULTI_PARTIAL_SPAN, tagValue = true))
93-
.clearLogs().addAllLogs(allLogs.asJava)
94-
.build())
95-
}
96-
}
97-
98-
// Network delta - difference between server and client duration
99-
// calculate only if serverDuration is smaller then client
100-
private def calculateNetworkDelta(clientSpan: Span, serverSpan: Span): Option[Long] = {
101-
val clientDuration = SpanUtils.getEventTimestamp(clientSpan, SpanMarkers.CLIENT_RECV_EVENT) - SpanUtils.getEventTimestamp(clientSpan, SpanMarkers.CLIENT_SEND_EVENT)
102-
val serverDuration = SpanUtils.getEventTimestamp(serverSpan, SpanMarkers.SERVER_SEND_EVENT) - SpanUtils.getEventTimestamp(serverSpan, SpanMarkers.SERVER_RECV_EVENT)
103-
104-
// difference of duration of spans
105-
if (serverDuration < clientDuration) {
106-
Some(clientDuration - serverDuration)
107-
} else {
108-
None
109-
}
110-
}
111-
112-
private def auxiliaryCommonTags(clientSpan: Span, serverSpan: Span): List[Tag] =
113-
List(
114-
buildBoolTag(AuxiliaryTags.IS_MERGED_SPAN, tagValue = true),
115-
buildLongTag(AuxiliaryTags.NETWORK_DELTA, calculateNetworkDelta(clientSpan, serverSpan).getOrElse(-1))
116-
)
117-
118-
private def auxiliaryClientTags(span: Span): List[Tag] =
119-
List(
120-
buildStringTag(AuxiliaryTags.CLIENT_SERVICE_NAME, span.getServiceName),
121-
buildStringTag(AuxiliaryTags.CLIENT_OPERATION_NAME, span.getOperationName),
122-
buildStringTag(AuxiliaryTags.CLIENT_INFRASTRUCTURE_PROVIDER, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_PROVIDER)),
123-
buildStringTag(AuxiliaryTags.CLIENT_INFRASTRUCTURE_LOCATION, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_LOCATION)),
124-
buildLongTag(AuxiliaryTags.CLIENT_START_TIME, span.getStartTime),
125-
buildLongTag(AuxiliaryTags.CLIENT_DURATION, span.getDuration)
126-
)
127-
128-
private def auxiliaryServerTags(span: Span): List[Tag] = {
129-
List(
130-
buildStringTag(AuxiliaryTags.SERVER_SERVICE_NAME, span.getServiceName),
131-
buildStringTag(AuxiliaryTags.SERVER_OPERATION_NAME, span.getOperationName),
132-
buildStringTag(AuxiliaryTags.SERVER_INFRASTRUCTURE_PROVIDER, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_PROVIDER)),
133-
buildStringTag(AuxiliaryTags.SERVER_INFRASTRUCTURE_LOCATION, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_LOCATION)),
134-
buildLongTag(AuxiliaryTags.SERVER_START_TIME, span.getStartTime),
135-
buildLongTag(AuxiliaryTags.SERVER_DURATION, span.getDuration)
136-
)
137-
}
13839
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2018 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expedia.www.haystack.trace.reader.readers.transformers
18+
19+
import com.expedia.www.haystack.trace.reader.readers.utils.{MutableSpanForest, SpanMerger}
20+
21+
class ServerClientSpanMergeTransformer extends SpanTreeTransformer {
22+
override def transform(spanForest: MutableSpanForest): MutableSpanForest = {
23+
spanForest.collapse((tree) =>
24+
tree.children match {
25+
case Seq(singleChild) if singleChild.span.getServiceName != tree.span.getServiceName && !SpanMerger.isAlreadyMergedSpan(tree.span) && !SpanMerger.isAlreadyMergedSpan(singleChild.span) =>
26+
Some(SpanMerger.mergeParentChildSpans(tree.span, singleChild.span))
27+
case _ => None
28+
})
29+
30+
spanForest
31+
}
32+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2018 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expedia.www.haystack.trace.reader.readers.utils
18+
19+
import com.expedia.open.tracing.{Span, Tag}
20+
import com.expedia.www.haystack.trace.reader.readers.utils.TagBuilders.{buildBoolTag, buildLongTag, buildStringTag}
21+
import com.expedia.www.haystack.trace.reader.readers.utils.TagExtractors.extractTagStringValue
22+
23+
import scala.collection.JavaConverters._
24+
25+
object SpanMerger {
26+
27+
def mergeParentChildSpans(parentSpan: Span, childSpan: Span): Span = {
28+
val clientSpan = if (SpanUtils.containsClientLogTag(parentSpan)) parentSpan else SpanUtils.addClientLogTag(parentSpan)
29+
val serverSpan = if (SpanUtils.containsServerLogTag(childSpan)) childSpan else SpanUtils.addServerLogTag(childSpan)
30+
merge(clientSpan, serverSpan)
31+
}
32+
33+
def mergeSpans(spans: Seq[Span]): Span = {
34+
val serverSpanOptional = collapseSpans(spans.filter(SpanUtils.containsServerLogTag))
35+
val clientSpanOptional = collapseSpans(spans.filter(SpanUtils.containsClientLogTag))
36+
(clientSpanOptional, serverSpanOptional) match {
37+
// ideally there should be one server and one client span
38+
// merging these partial spans to form a new single span
39+
case (Some(clientSpan), Some(serverSpan)) => merge(clientSpan, serverSpan)
40+
41+
// imperfect scenario, fallback to return available server span
42+
case (None, Some(serverSpan)) => serverSpan
43+
44+
// imperfect scenario, fallback to return available client span
45+
case (Some(clientSpan), None) => clientSpan
46+
47+
// imperfect scenario, fallback to collapse all spans
48+
case _ => collapseSpans(spans).get
49+
}
50+
}
51+
52+
private def merge(clientSpan: Span, serverSpan: Span): Span = {
53+
Span
54+
.newBuilder(serverSpan)
55+
.setParentSpanId(clientSpan.getParentSpanId) // use the parentSpanId of the client span to stitch in the client's trace tree
56+
.addAllTags((clientSpan.getTagsList.asScala ++ auxiliaryCommonTags(clientSpan, serverSpan) ++ auxiliaryClientTags(clientSpan) ++ auxiliaryServerTags(serverSpan)).asJavaCollection)
57+
.clearLogs().addAllLogs((clientSpan.getLogsList.asScala ++ serverSpan.getLogsList.asScala.sortBy(_.getTimestamp)).asJavaCollection)
58+
.build()
59+
}
60+
61+
// collapse all spans of a type(eg. client or server) if needed,
62+
// ideally there would be just one span in the list and hence no need of collapsing
63+
private def collapseSpans(spans: Seq[Span]): Option[Span] = {
64+
spans match {
65+
case Nil => None
66+
case Seq(span) => Some(span)
67+
case _ =>
68+
// if there are multiple spans fallback to collapse all the spans in a single span
69+
// start the collapsed span from startTime of the first and end at ending of last such span
70+
// also add an error marker in the collapsed span
71+
val firstSpan = spans.minBy(_.getStartTime)
72+
val lastSpan = spans.maxBy(span => span.getStartTime + span.getDuration)
73+
val allTags = spans.flatMap(span => span.getTagsList.asScala)
74+
val allLogs = spans.flatMap(span => span.getLogsList.asScala)
75+
val opName = spans.map(_.getOperationName).reduce((a, b) => a + " & " + b)
76+
77+
Some(
78+
Span
79+
.newBuilder(firstSpan)
80+
.setOperationName(opName)
81+
.setDuration(lastSpan.getStartTime + lastSpan.getDuration - firstSpan.getStartTime)
82+
.clearTags().addAllTags(allTags.asJava)
83+
.addTags(buildBoolTag(AuxiliaryTags.ERR_IS_MULTI_PARTIAL_SPAN, tagValue = true))
84+
.clearLogs().addAllLogs(allLogs.asJava)
85+
.build())
86+
}
87+
}
88+
89+
// Network delta - difference between server and client duration
90+
// calculate only if serverDuration is smaller then client
91+
private def calculateNetworkDelta(clientSpan: Span, serverSpan: Span): Option[Long] = {
92+
val clientDuration = SpanUtils.getEventTimestamp(clientSpan, SpanMarkers.CLIENT_RECV_EVENT) - SpanUtils.getEventTimestamp(clientSpan, SpanMarkers.CLIENT_SEND_EVENT)
93+
val serverDuration = SpanUtils.getEventTimestamp(serverSpan, SpanMarkers.SERVER_SEND_EVENT) - SpanUtils.getEventTimestamp(serverSpan, SpanMarkers.SERVER_RECV_EVENT)
94+
95+
// difference of duration of spans
96+
if (serverDuration < clientDuration) {
97+
Some(clientDuration - serverDuration)
98+
} else {
99+
None
100+
}
101+
}
102+
103+
private def auxiliaryCommonTags(clientSpan: Span, serverSpan: Span): List[Tag] =
104+
List(
105+
buildBoolTag(AuxiliaryTags.IS_MERGED_SPAN, tagValue = true),
106+
buildLongTag(AuxiliaryTags.NETWORK_DELTA, calculateNetworkDelta(clientSpan, serverSpan).getOrElse(-1))
107+
)
108+
109+
private def auxiliaryClientTags(span: Span): List[Tag] =
110+
List(
111+
buildStringTag(AuxiliaryTags.CLIENT_SERVICE_NAME, span.getServiceName),
112+
buildStringTag(AuxiliaryTags.CLIENT_OPERATION_NAME, span.getOperationName),
113+
buildStringTag(AuxiliaryTags.CLIENT_INFRASTRUCTURE_PROVIDER, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_PROVIDER)),
114+
buildStringTag(AuxiliaryTags.CLIENT_INFRASTRUCTURE_LOCATION, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_LOCATION)),
115+
buildLongTag(AuxiliaryTags.CLIENT_START_TIME, span.getStartTime),
116+
buildLongTag(AuxiliaryTags.CLIENT_DURATION, span.getDuration)
117+
)
118+
119+
private def auxiliaryServerTags(span: Span): List[Tag] = {
120+
List(
121+
buildStringTag(AuxiliaryTags.SERVER_SERVICE_NAME, span.getServiceName),
122+
buildStringTag(AuxiliaryTags.SERVER_OPERATION_NAME, span.getOperationName),
123+
buildStringTag(AuxiliaryTags.SERVER_INFRASTRUCTURE_PROVIDER, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_PROVIDER)),
124+
buildStringTag(AuxiliaryTags.SERVER_INFRASTRUCTURE_LOCATION, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_LOCATION)),
125+
buildLongTag(AuxiliaryTags.SERVER_START_TIME, span.getStartTime),
126+
buildLongTag(AuxiliaryTags.SERVER_DURATION, span.getDuration)
127+
)
128+
}
129+
130+
def isAlreadyMergedSpan(span: Span): Boolean = {
131+
span.getTagsList.asScala.exists(tag => tag.getKey.equals(AuxiliaryTags.IS_MERGED_SPAN))
132+
}
133+
}

reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanTree.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,33 @@ case class MutableSpanForest(private var spans: Seq[Span]) {
128128
})
129129
}
130130
}
131+
132+
def collapse(applyCondition: (SpanTree) => Option[Span]): Unit = {
133+
val underlyingSpans = mutable.ListBuffer[Span]()
134+
135+
def collapseTree(spanTree: SpanTree): Unit = {
136+
val queue = mutable.Queue[SpanTree]()
137+
queue.enqueue(spanTree)
138+
139+
while (queue.nonEmpty) {
140+
val tree = queue.dequeue()
141+
applyCondition(tree) match {
142+
case Some(mergedSpan) =>
143+
tree.span = mergedSpan
144+
val childSpanTrees = new ListBuffer[SpanTree]()
145+
tree.children.foreach(t => childSpanTrees.appendAll(t.children))
146+
tree.children.clear()
147+
childSpanTrees.foreach(tr => tree.children.append(tr))
148+
case _ =>
149+
}
150+
underlyingSpans.append(tree.span)
151+
queue.enqueue(tree.children:_*)
152+
}
153+
}
154+
155+
getAllTrees.foreach(collapseTree)
156+
updateUnderlyingSpans(underlyingSpans, triggerForestUpdate = false)
157+
}
131158
}
132159

133160
case class SpanTree(var span: Span, children: mutable.ListBuffer[SpanTree] = mutable.ListBuffer[SpanTree]())

0 commit comments

Comments
 (0)