Skip to content

Commit

Permalink
Add bound context as default context parameter
Browse files Browse the repository at this point in the history
Bound context is a new opt-in feature which leverages a Clojure dynamic
var. It overrides current context as the default `context` or `parent`
parameter value in `clj-otel` functions. This is intended to simplify
async code by eliminating explicit context passing.

Existing code that uses the current context or explicit context
parameters is unaffected by the introduction of the bound context.
  • Loading branch information
steffan-westcott committed Jun 4, 2023
1 parent afdb479 commit 680cdb2
Show file tree
Hide file tree
Showing 20 changed files with 1,159 additions and 93 deletions.
8 changes: 5 additions & 3 deletions .zprint.edn
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
"finally" :flow-body

;; clj-otel-api
"bind-context!" :arg1-body
"with-bound-context!" :flow-body
"with-bound-span!" :arg1-body
"with-context!" :arg1-body
"with-value!" :arg1-body
"with-span!" :arg1-body
"with-span-binding" :binding
"with-span-binding'" :binding
"with-value!" :arg1-body

;; examples
"catch-all" :flow-body
Expand All @@ -36,5 +39,4 @@
"<with-span-binding" :binding

;; core.async
"async/go" :flow-body
"async/go-loop" :binding}}
"go" :flow-body}}
3 changes: 3 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ WARNING: Until version `1.0.0` there is a greater possibility of breaking change

=== `0.2.2` (in progress)

- [ADD] Opt-in feature xref:doc/concepts.adoc#_bound_context[bound context], a Clojure dynamic var that overrides the default `context` or `parent` parameter value for `clj-otel` functions.
This feature is intended to simplify asynchronous code by eliminating explicit context passing.
Existing code that uses the current context or explicit context parameter values is unaffected.
- [FIX] Convert key names of entries added to OpenTelemetry attributes to snake_case.
This applies to resources, spans and metrics.
- [FIX] Do not transform key names of entries added to OpenTelemetry context or baggage.
Expand Down
6 changes: 3 additions & 3 deletions clj-otel-api/src/steffan_westcott/clj_otel/api/baggage.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
(:import (io.opentelemetry.api.baggage Baggage BaggageBuilder BaggageEntry BaggageEntryMetadata)))

(defn get-baggage
"Gets the baggage from a given context, or the current context if none is
given. If no baggage is found in the context, empty baggage is returned."
"Gets the baggage from a given context, or the bound ot current context if
none is given. If no baggage is found in the context, empty baggage is returned."
([]
(Baggage/current))
(get-baggage (context/dyn)))
([context]
(Baggage/fromContext context)))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
:description "The size of HTTP response messages"})))

(defn- add-active-requests!
([value attrs] (add-active-requests! value attrs (context/current)))
([value attrs] (add-active-requests! value attrs (context/dyn)))
([value attrs context]
(instrument/add! @active-requests
{:value value
Expand All @@ -80,16 +80,15 @@

(defn- record-duration!
([start-time server-request-attrs status]
(record-duration! start-time server-request-attrs status (context/current)))
(record-duration! start-time server-request-attrs status (context/dyn)))
([start-time server-request-attrs status context]
(instrument/record! @request-duration
{:value (since-millis! start-time)
:attributes (request-duration-or-size-attrs server-request-attrs status)
:context context})))

(defn- record-request-size!
([server-request-attrs status]
(record-request-size! server-request-attrs status (context/current)))
([server-request-attrs status] (record-request-size! server-request-attrs status (context/dyn)))
([server-request-attrs status context]
(when-let [size (get server-request-attrs SemanticAttributes/HTTP_REQUEST_CONTENT_LENGTH)]
(instrument/record! @request-size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@
| key | description |
|-------------|-------------|
|`:context` | Context to associate with delta (default: current context).
|`:context` | Context to associate with delta (default: bound or current context).
|`:value` | `long` or `double` value to add to `counter` (required). Must not be negative for `:counter` instruments.
|`:attributes`| Map of attributes to attach to delta (default: no attributes)."))

(defmacro ^:private add*
[counter delta]
`(let [{:keys [~'context ~'value ~'attributes]
:or {~'context (context/current)}}
:or {~'context (context/dyn)}}
~delta]
(.add ~counter ~'value (attr/->attributes ~'attributes) ~'context)))

Expand All @@ -107,14 +107,14 @@
| key | description |
|-------------|-------------|
|`:context` | Context to associate with measurement (default: current context).
|`:context` | Context to associate with measurement (default: bound or current context).
|`:value` | `long` or `double` value to record in `histogram` (required).
|`:attributes`| Map of attributes to attach to measurement (default: no attributes)."))

(defmacro ^:private record*
[histogram measurement]
`(let [{:keys [~'context ~'value ~'attributes]
:or {~'context (context/current)}}
:or {~'context (context/dyn)}}
~measurement]
(.record ~histogram ~'value (attr/->attributes ~'attributes) ~'context)))

Expand Down
49 changes: 33 additions & 16 deletions clj-otel-api/src/steffan_westcott/clj_otel/api/trace/http.clj
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
(str method " " app-root "/*")
method))
:span-kind :server
;; always merge extracted context with current context
;; always merge extracted context with bound or current context
:parent (context/headers->merged-context headers)
:attributes server-request-attrs})))

Expand All @@ -89,12 +89,12 @@
| key | description |
|----------|-------------|
|`:parent` | Context used to take parent span. If `nil` or no span is available in the context, the root context is used instead (default: use current context)."
|`:parent` | Context used to take parent span. If `nil` or no span is available in the context, the root context is used instead (default: use bound or current context)."
([request]
(client-span-opts request {}))
([request
{:keys [parent]
:or {parent (context/current)}}]
:or {parent (context/dyn)}}]
(let [{:keys [method]} request
method' (str/upper-case (name method))]
{:name method'
Expand All @@ -113,13 +113,13 @@
| key | description |
|-----------|-------------|
|`:context` | Context containing server span (default: current context).
|`:context` | Context containing server span (default: bound or current context).
|`:app-root`| Web application root, a URL prefix for all HTTP routes served by this application e.g. `\"/webshop\"` (default: `nil`)."
([request-method route]
(add-route-data! request-method route {}))
([request-method route
{:keys [context app-root]
:or {context (context/current)}}]
:or {context (context/dyn)}}]
(when route
(span/add-span-data!
{:context context
Expand All @@ -132,12 +132,12 @@
| key | description |
|-----------|-------------|
|`:context` | Context containing span to add response data to (default: current context)."
|`:context` | Context containing span to add response data to (default: bound or current context)."
([response]
(add-client-span-response-data! response {}))
([response
{:keys [context]
:or {context (context/current)}}]
:or {context (context/dyn)}}]
(let [{:keys [status headers]} response
{:strs [Content-Length]} headers
Content-Length' (when Content-Length
Expand All @@ -157,12 +157,12 @@
| key | description |
|-----------|-------------|
|`:context` | Context containing span to add response data to (default: current context)."
|`:context` | Context containing span to add response data to (default: bound or current context)."
([response]
(add-server-span-response-data! response {}))
([response
{:keys [context]
:or {context (context/current)}}]
:or {context (context/dyn)}}]
(let [{:keys [status io.opentelemetry.api.trace.span.status/description]} response
attrs {SemanticAttributes/HTTP_STATUS_CODE status}
stat (when (<= 500 status)
Expand Down Expand Up @@ -222,13 +222,22 @@
([request]
(handler request))
([request respond raise]
(let [context (context/current)]
(let [context (context/dyn)]
(handler (assoc request :io.opentelemetry/server-span-context context)
respond
(fn [e]
(span/add-exception! e {:context context})
(raise e)))))))

(defn- wrap-bound-context
[handler]
(fn
([request]
(handler request))
([request respond raise]
(context/bind-context! (:io.opentelemetry/server-span-context request)
(handler request respond raise)))))

(defn wrap-server-span
"Ring middleware to add HTTP server span support. This middleware can be
configured to either use existing server spans created by the OpenTelemetry
Expand All @@ -249,8 +258,8 @@
`:io.opentelemetry.api.trace.span.status/description` in the response map.
No matter how the server span is created, for an asynchronous handler the
context containing the server span is set as the value of
`:io.opentelemetry/server-span-context` in the request map.
bound context and key `:io.opentelemetry/server-span-context` in the request
map are set to the context containing the server span.
May take an option map as follows:
Expand All @@ -265,6 +274,7 @@
{:keys [create-span?]
:as create-span-opts}]
(cond-> handler
:always (wrap-bound-context)
create-span? (wrap-new-server-span create-span-opts)
create-span? (wrap-server-request-attrs create-span-opts)
(not create-span?) (wrap-existing-server-span))))
Expand Down Expand Up @@ -325,7 +335,7 @@
[]
{:name ::existing-server-span
:enter (fn [ctx]
(assoc ctx :io.opentelemetry/server-span-context (context/current)))
(assoc ctx :io.opentelemetry/server-span-context (context/dyn)))
:error (fn [{:keys [io.opentelemetry/server-span-context]
:as ctx} e]
(span/add-interceptor-exception! e {:context server-span-context})
Expand Down Expand Up @@ -354,6 +364,11 @@
(context/current-context-interceptor :io.opentelemetry/server-span-context
:io.opentelemetry/server-span-scope))

(defn- bound-context-interceptor
[]
(context/bound-context-interceptor :io.opentelemetry/server-span-context))


(defn server-span-interceptors
"Returns a vector of Pedestal interceptor maps that add HTTP server span
support to subsequent execution of the interceptor chain for an HTTP service.
Expand All @@ -378,8 +393,9 @@
`:io.opentelemetry.api.trace.span.status/description` in the response map.
No matter how the server span is created, the context containing the server
span is set as the value of `:io.opentelemetry/server-span-context` in both
the interceptor context and request maps.
span is set as the bound context and the value of
`:io.opentelemetry/server-span-context` in both the interceptor context and
request maps.
May take an option map as follows:
Expand All @@ -401,7 +417,8 @@
(not create-span?) (conj (existing-server-span-interceptor))
:always (conj (execution-id-interceptor))
:always (conj (copy-context-interceptor))
(and create-span? set-current-context?) (conj (current-context-interceptor)))))
(and create-span? set-current-context?) (conj (current-context-interceptor))
:always (conj (bound-context-interceptor)))))

(defn route-interceptor
"Returns a Pedestal interceptor that adds a matched route to the server span
Expand Down
Loading

0 comments on commit 680cdb2

Please sign in to comment.