Skip to content

Commit a949205

Browse files
authored
Merge pull request #17 from Yleisradio/fix-trace-context-propagation
Fix trace context propagation
2 parents 06fb781 + add4ee8 commit a949205

File tree

7 files changed

+139
-26
lines changed

7 files changed

+139
-26
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v4
1616

17-
- run: docker-compose up --detach
17+
- run: docker compose up --detach
1818

1919
- uses: actions/setup-java@v4
2020
with:

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ The options hash for `with-apm-transaction` accepts the following options:
5656
* `:labels` - `{String any}` - map or sequence of label names and values to add to the transaction
5757
* `:tags` - `{String String}` - ~~map or sequence of label names and values to add to the transaction~~ Deprecated as of 0.5.0
5858
* `:activate?` - `Boolean` - whether to make the transaction active in the context of the executing thread (defaults to true). When activated, calling `apm/current-apm-transaction` returns this transaction.
59-
* `:traceparent` - `String` - the trace id when using APM's [Distributed tracing](https://www.elastic.co/guide/en/apm/get-started/current/distributed-tracing.html). Usually value is passed within HTTP request headers.
59+
* `:headers` - `{String String}` - a map of [trace context headers](https://www.w3.org/TR/trace-context/) (i.e. `traceparent` and `tracestate`) for [distributed tracing](https://www.elastic.co/guide/en/apm/get-started/current/distributed-tracing.html).
60+
* `:traceparent` - `String` - ~~the trace id when using APM's [Distributed tracing](https://www.elastic.co/guide/en/apm/get-started/current/distributed-tracing.html). Usually value is passed within HTTP request headers.~~ Deprecated as of 0.13.0
6061

6162
The options hash for `with-apm-span` accepts the following options:
6263

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: "3"
22
services:
33
elasticsearch:
4-
image: "docker.elastic.co/elasticsearch/elasticsearch:7.16.0"
4+
image: "docker.elastic.co/elasticsearch/elasticsearch:8.16.0"
55
container_name: "elasticsearch"
66
environment:
77
- xpack.security.enabled=false
@@ -14,10 +14,10 @@ services:
1414
- "9200:9200"
1515
- "9300:9300"
1616
kibana:
17-
image: "docker.elastic.co/kibana/kibana:7.16.0"
17+
image: "docker.elastic.co/kibana/kibana:8.16.0"
1818
ports:
1919
- "5601:5601"
2020
apm-server:
21-
image: "docker.elastic.co/apm/apm-server:7.16.0"
21+
image: "docker.elastic.co/apm/apm-server:8.16.0"
2222
ports:
2323
- "8200:8200"

project.clj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(defproject clojure-elastic-apm "0.12.0"
1+
(defproject clojure-elastic-apm "0.13.0"
22
:description "Clojure wrapper for Elastic APM Java Agent"
33
:url "https://github.com/Yleisradio/clojure-elastic-apm"
44
:license {:name "Eclipse Public License"
@@ -9,6 +9,6 @@
99
"-Delastic.apm.application_packages=clojure-elastic-apm"
1010
"-Delastic.apm.server_urls=http://localhost:8200"
1111
"-Delastic.apm.metrics_interval=1s"]
12-
:dependencies [[clj-http "3.12.3"]
13-
[cheshire "5.11.0"]]}
14-
:provided {:dependencies [[co.elastic.apm/apm-agent-api "1.38.0"]]}})
12+
:dependencies [[clj-http "3.13.0"]
13+
[cheshire "5.13.0"]]}
14+
:provided {:dependencies [[co.elastic.apm/apm-agent-api "1.52.1"]]}})

src/clojure_elastic_apm/core.clj

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(ns clojure-elastic-apm.core
2-
(:import [co.elastic.apm.api ElasticApm Transaction Span HeaderExtractor Outcome]))
2+
(:require [clojure.string :as string])
3+
(:import [co.elastic.apm.api ElasticApm Transaction Span HeaderExtractor HeadersExtractor Outcome]))
34

45
(set! *warn-on-reflection* true)
56

@@ -13,10 +14,7 @@
1314
(.setLabel span-or-tx (name k) (str v)))
1415

1516
(defn set-label [^Span span-or-tx k v]
16-
(cond
17-
(number? v) (.setLabel span-or-tx (name k) ^Number v)
18-
(boolean? v) (.setLabel span-or-tx (name k) ^boolean v)
19-
:else (.setLabel span-or-tx (name k) (str v))))
17+
(.setLabel span-or-tx (name k) (str v)))
2018

2119
(defn set-name [^Span span-or-tx name]
2220
(.setName span-or-tx name))
@@ -42,12 +40,24 @@
4240
(defn set-outcome-unknown [^Span span-or-tx]
4341
(set-outcome span-or-tx outcome-unknown))
4442

45-
(defn trace-extractor ^HeaderExtractor [traceparent]
43+
(defn ^:deprecated trace-extractor
44+
"DEPRECATED: Do not use. This function sets the value of every trace context
45+
header (i.e. both \"traceparent\" and \"tracestate\") to the value you pass
46+
it."
47+
^HeaderExtractor [traceparent]
4648
(reify HeaderExtractor
4749
(getFirstHeader [_ _] traceparent)))
4850

4951
(def type-request Transaction/TYPE_REQUEST)
5052

53+
(defn- header-values [headers header-name]
54+
;; "Where there are multiple headers with the same name, the adapter must
55+
;; concatenate the values into a single string, using the ASCII `,` character
56+
;; as a delimiter."
57+
;;
58+
;; -- https://github.com/ring-clojure/ring/blob/master/SPEC.md#headers
59+
(some-> (get headers header-name) (string/split #",")))
60+
5161
(defn start-transaction
5262
([]
5363
(start-transaction {}))
@@ -56,9 +66,25 @@
5666
^String tx-type :type
5767
tags :tags
5868
labels :labels
59-
traceparent :traceparent}]
60-
(let [tx (if traceparent
61-
(ElasticApm/startTransactionWithRemoteParent (trace-extractor traceparent))
69+
headers :headers
70+
:as options}]
71+
(let [tx (cond
72+
headers
73+
(ElasticApm/startTransactionWithRemoteParent
74+
(reify HeaderExtractor
75+
(getFirstHeader [_ name]
76+
(first (header-values headers name))))
77+
(reify HeadersExtractor
78+
(getAllHeaders [_ name]
79+
(header-values headers name))))
80+
81+
;; Retained only for backwards compatibility. Use :headers
82+
;; instead.
83+
(:traceparent options)
84+
(ElasticApm/startTransactionWithRemoteParent
85+
(trace-extractor (:traceparent options)))
86+
87+
:else
6288
(ElasticApm/startTransaction))]
6389
(when tx-name
6490
(set-name tx tx-name))

src/clojure_elastic_apm/ring.clj

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,19 @@
3939
(fn
4040
([{:keys [request-method uri headers] :as request}]
4141
(let [matched (match-patterns patterns uri)
42-
tx-name (str (string/upper-case (name request-method)) " " matched)
43-
traceparent (get headers "traceparent")]
42+
tx-name (str (string/upper-case (name request-method)) " " matched)]
4443
(if matched
45-
(with-apm-transaction [^Transaction tx {:name tx-name :type type-request :traceparent traceparent}]
44+
(with-apm-transaction [^Transaction tx {:name tx-name :type type-request :headers headers}]
4645
(let [{:keys [status] :as response} (handler (assoc request :clojure-elastic-apm/transaction tx))]
4746
(when status
4847
(.setResult tx (str "HTTP " status)))
4948
response))
5049
(handler request))))
5150
([{:keys [request-method uri headers] :as request} respond raise]
5251
(let [matched (match-patterns patterns uri)
53-
tx-name (str (string/upper-case (name request-method)) " " matched)
54-
traceparent (get headers "traceparent")]
52+
tx-name (str (string/upper-case (name request-method)) " " matched)]
5553
(if matched
56-
(let [tx (apm/start-transaction {:name tx-name :type type-request :traceparent traceparent})
54+
(let [tx (apm/start-transaction {:name tx-name :type type-request :headers headers})
5755
req (assoc request :clojure-elastic-apm/transaction tx)]
5856
(with-open [_ (apm/activate tx)]
5957
(handler req

test/clojure_elastic_apm/core_test.clj

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
(let [tx-details (es-find-first-document (str "(processor.event:transaction%20AND%20transaction.id:" @transaction-id ")"))]
3232
(is (= "TestTransaction" (get-in tx-details [:transaction :name])))
3333
(is (= "1" (get-in tx-details [:labels :t1])))
34-
(is (= 2 (get-in tx-details [:labels :t2])))
35-
(is (true? (get-in tx-details [:labels :t3])))
34+
(is (= "2" (get-in tx-details [:labels :t2])))
35+
(is (= "true" (get-in tx-details [:labels :t3])))
3636
(is (= "Label 4" (get-in tx-details [:labels :t4])))
3737
(is (= "test-result" (get-in tx-details [:transaction :result])))
3838
(is (= "success" (get-in tx-details [:event :outcome]))))))
@@ -159,3 +159,91 @@
159159
(-> doc
160160
(select-keys [:span])
161161
(update :span (fn [span] (select-keys span [:name :type])))))))))
162+
163+
(defn trace-context
164+
"Given an APM span, return the trace context[1] HTTP headers of the span.
165+
166+
[1]: https://www.w3.org/TR/trace-context/"
167+
[span]
168+
(let [headers (transient {})]
169+
(.injectTraceHeaders span
170+
(reify co.elastic.apm.api.HeaderInjector
171+
(addHeader [_ name value]
172+
(assoc! headers name value))))
173+
(persistent! headers)))
174+
175+
(def ^:private traceparent-regex
176+
"https://www.w3.org/TR/trace-context/#traceparent-header"
177+
#"^([0-9a-fA-F]{2})-([0-9a-fA-F]{32})-([0-9a-fA-F]{16})-([0-9a-fA-F]{2})$")
178+
179+
(def ^:private tracestate-regex
180+
"https://www.w3.org/TR/trace-context/#tracestate-header"
181+
#"^[!-~]+(,[!-~]+=[!-~]+)*$")
182+
183+
(defn parse-traceparent-header
184+
"Given a traceparent header, parse the header value into its constituents."
185+
[traceparent]
186+
(zipmap [:version :trace-id :parent-id :trace-flags]
187+
(rest (re-matches traceparent-regex traceparent))))
188+
189+
(defn fields-except
190+
"Given a traceparent header, return a map of its constituents, except fields."
191+
[traceparent & fields]
192+
(-> (parse-traceparent-header traceparent)
193+
(apply dissoc fields)))
194+
195+
(deftest trace-context-test
196+
(testing "no opts"
197+
(let [tx (apm/start-transaction)
198+
{:strs [traceparent tracestate]} (trace-context tx)]
199+
;; APM adds new trace context headers.
200+
(is (re-matches traceparent-regex traceparent))
201+
(is (re-matches tracestate-regex tracestate))))
202+
203+
(testing ":headers -- empty & nil"
204+
(let [tx (apm/start-transaction {:headers {}})
205+
{:strs [traceparent tracestate]} (trace-context tx)]
206+
(is (re-matches traceparent-regex traceparent))
207+
(is (re-matches tracestate-regex tracestate)))
208+
209+
(let [tx (apm/start-transaction {:headers nil})
210+
{:strs [traceparent tracestate]} (trace-context tx)]
211+
(is (re-matches traceparent-regex traceparent))
212+
(is (re-matches tracestate-regex tracestate))))
213+
214+
(testing ":headers -- traceparent only"
215+
(let [inbound-traceparent "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
216+
tx (apm/start-transaction {:headers {"traceparent" inbound-traceparent}})
217+
{:strs [traceparent]} (trace-context tx)]
218+
(is (= (fields-except inbound-traceparent :parent-id)
219+
(fields-except traceparent :parent-id)))))
220+
221+
(testing ":headers -- traceparent & tracestate"
222+
(let [inbound-traceparent "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
223+
inbound-tracestate "es=s:0.25"
224+
tx (apm/start-transaction {:headers {"traceparent" inbound-traceparent
225+
"tracestate" inbound-tracestate}})
226+
{:strs [traceparent tracestate]} (trace-context tx)]
227+
(is (= (fields-except inbound-traceparent :parent-id)
228+
(fields-except traceparent :parent-id)))
229+
(is (= inbound-tracestate tracestate))))
230+
231+
(testing ":headers -- multi-value tracestate"
232+
(let [inbound-traceparent "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
233+
inbound-tracestate "es=s:0.5,rojo=00f067aa0ba902b7"
234+
tx (apm/start-transaction {:headers {"traceparent" inbound-traceparent
235+
"tracestate" inbound-tracestate}})
236+
{:strs [tracestate]} (trace-context tx)]
237+
(is (= inbound-tracestate tracestate))))
238+
239+
(testing ":traceparent (backwards compatibility only, do not use)"
240+
(let [inbound-traceparent "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
241+
tx (apm/start-transaction {:traceparent inbound-traceparent})
242+
{:strs [traceparent tracestate]} (trace-context tx)]
243+
(is (= (fields-except inbound-traceparent :parent-id)
244+
(fields-except traceparent :parent-id)))
245+
246+
;; Using the :traceparent option incorrectly sets the value of every
247+
;; trace context header -- including tracestate -- to the value of the
248+
;; traceparent header. Do not use it.
249+
(is (= inbound-traceparent tracestate)))))

0 commit comments

Comments
 (0)