[new] Add basic OpenTelemetry handler

This commit is contained in:
Peter Taoussanis 2024-04-08 16:55:42 +02:00
parent 2abb9de61b
commit be4644220c
4 changed files with 333 additions and 11 deletions

View file

@ -388,9 +388,17 @@
#?(:cljs handlers:console/handler:console-raw)
#?(:clj handlers:file/handler:file))
#?(:clj
(enc/compile-when
(do (require '[taoensso.telemere.handlers.open-telemetry :as handlers:open-tel]))
(enc/defalias handlers:open-tel/handler:open-telemetry-logger)))
(defonce ^:no-doc __add-default-handlers
(do
(add-handler! :default/console (handler:console))
#?(:clj
(enc/compile-when handler:open-telemetry-logger
(add-handler! :default/open-telemetry-logger handler:open-telemetry-logger)))
nil))
;;;; Flow benchmarks

View file

@ -0,0 +1,216 @@
(ns ^:no-doc taoensso.telemere.handlers.open-telemetry
"Private ns, implementation detail.
Core OpenTelemetry handlers.
Needs `OpenTelemetry Java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>."
(:require
[clojure.string :as str]
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.utils :as utils])
(:import
[io.opentelemetry.api.logs LoggerProvider Severity]
[io.opentelemetry.api.common Attributes AttributesBuilder]
[io.opentelemetry.api GlobalOpenTelemetry]))
(comment
(remove-ns 'taoensso.telemere.handlers.open-telemetry)
(:api (enc/interns-overview)))
;;;; Implementation
(defn level->severity
^Severity [level]
(case level
:trace Severity/TRACE
:debug Severity/DEBUG
:info Severity/INFO
:warn Severity/WARN
:error Severity/ERROR
:fatal Severity/FATAL
:report Severity/INFO4
Severity/UNDEFINED_SEVERITY_NUMBER))
(def ^String attr-name
"Returns cached OpenTelemetry-style name: `:foo/bar-baz` -> \"foo_bar_baz\", etc.
Ref. <https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/>"
(enc/fmemoize
(fn
([prefix x] (str (attr-name prefix) "." (attr-name x))) ; For `merge-prefix-map`, etc.
([ x]
(if-not (enc/named? x)
(str/replace (str/lower-case (str x)) #"[-\s]" "_")
(if-let [ns (namespace x)]
(str/replace (str/lower-case (str ns "." (name x))) "-" "_")
(str/replace (str/lower-case (name x)) "-" "_")))))))
(comment (enc/qb 1e6 (attr-name :x1.x2/x3-x4 :Foo/Bar-BAZ))) ; 63.6
;; AttributeTypes: String, Long, Double, Boolean, and arrays
(defprotocol IAttr+ (attr+ [_aval akey builder]))
(extend-protocol IAttr+
nil (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) "nil")) ; Like pr-edn*
Boolean (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
String (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
clojure.lang.Named (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v str))) ; ":foo/bar", etc.
java.util.UUID (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v str))) ; "d4fc65a0..."
Long (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
Integer (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v long)))
Short (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v long)))
Byte (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v long)))
Double (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
Float (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v double)))
Number (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v double)))
clojure.lang.IPersistentCollection
(attr+ [v k ^AttributesBuilder b]
(let [v1 (first v)]
(or
(cond
(boolean? v1) (enc/catching :common (.put b (attr-name k) (boolean-array (mapv boolean v))))
(int? v1) (enc/catching :common (.put b (attr-name k) (long-array (mapv long v))))
(float? v1) (enc/catching :common (.put b (attr-name k) (double-array (mapv double v)))))
(do (.put b (attr-name k) ^"[Ljava.lang.String;" (into-array String (mapv enc/pr-edn* v)))))))
Object (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (enc/pr-edn* v))))
(defn as-attrs
"Returns `io.opentelemetry.api.common.Attributes` for given map."
^Attributes [m]
(if (empty? m)
(Attributes/empty)
(let [builder (Attributes/builder)]
(enc/run-kv! (fn [k v] (attr+ v k builder)) m)
(.build builder))))
(comment (str (as-attrs {:s "s", :kw :foo/bar, :long 5, :double 5.0, :longs [5 5 5] :nil nil})))
(defn merge-prefix-map
"Merges prefixed `from` into `to`."
[to prefix from]
(enc/cond
(map? from)
(reduce-kv
(fn [acc k v] (assoc acc (attr-name prefix k) v))
to from)
from (assoc to prefix from)
:else to))
(comment (merge-prefix-map {} "data" {:a/b1 "v1" :a/b2 "v2" :nil nil}))
(defn signal->attrs-map
"Returns attributes map for given signal.
Ref. <https://opentelemetry.io/docs/specs/otel/logs/data-model/>."
[extra-attrs-key signal]
(let [attrs-map
(let [{:keys [ns line file, kind level id uid parent,
run-form run-val run-nsecs, sample-rate]}
signal]
(enc/assoc-some nil
{"ns" ns
"line" line
"file" file
"error" (utils/error-signal? signal) ; Standard key
"kind" kind
"level" level
"id" id
"uid" uid
"run.form" run-form
"run.val_type" (enc/class-sym run-val)
"run.val" run-val
"run.nsecs" run-nsecs
"sample" sample-rate
"parent.id" (get parent :id)
"parent.uid" (get parent :uid)}))
attrs-map
(enc/if-not [{:keys [type msg data trace]} (enc/ex-map (get signal :error))]
attrs-map
(merge-prefix-map
(enc/assoc-some attrs-map
;; 3x standard keys
"exception.type" type
"exception.message" msg
"exception.stacktrace" (when trace (#'utils/format-clj-stacktrace trace)))
"exception.data" data))
extra-kvs (get signal :extra-kvs)
attr-kvs
(when extra-attrs-key
(when-let [kvs (get signal extra-attrs-key)]
(not-empty kvs)))
extra-kvs
(if attr-kvs
(dissoc extra-kvs extra-attrs-key)
(do extra-kvs))
attrs-map
(-> attrs-map
(merge-prefix-map "ctx" (get signal :ctx))
(merge-prefix-map "data" (get signal :data))
(merge-prefix-map "kvs" (get signal :extra-kvs))
(enc/fast-merge attr-kvs) ; Unprefixed, undocumented
)]
attrs-map))
(defn get-default-logger-provider
"Experimental, subject to change!! Feedback very welcome!
Returns `io.opentelemetry.api.logs.LoggerProvider` via:
`AutoConfiguredOpenTelemetrySdk` when possible, or
`GlobalOpenTelemetry` otherwise."
^LoggerProvider []
(or
(enc/compile-when
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
(enc/catching :common
(let [builder (io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk/builder)]
(.getSdkLoggerProvider (.getOpenTelemetrySdk (.build builder))))))
(.getLogsBridge (GlobalOpenTelemetry/get))))
;;;; Handler
(defn ^:public handler:open-telemetry-logger
"Experimental, subject to change!! Feedback very welcome!
Returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Emits signal content to the `io.opentelemetry.api.logs.Logger`
returned by given `io.opentelemetry.api.logs.LoggerProvider`."
([] (handler:open-telemetry-logger nil))
([{:keys [^LoggerProvider logger-provider
extra-attrs-key ; Undocumented
]
:or
{logger-provider (get-default-logger-provider)
extra-attrs-key :open-telemetry-attrs}}]
(let []
(fn a-handler:open-telemetry-logger
([]) ; Shut down (no-op)
([signal]
(let [{:keys [ns inst level msg_]} signal
logger (.get logger-provider (or ns "default"))
severity (level->severity level)
msg (force msg_)
attrs-map (signal->attrs-map extra-attrs-key signal)
attrs (as-attrs attrs-map)]
(.emit
(doto (.logRecordBuilder logger)
(.setTimestamp inst)
(.setSeverity severity)
(.setBody msg)
(.setAllAttributes attrs)))))))))

View file

@ -257,6 +257,18 @@
(comment ((format-inst-fn) (enc/now-inst)))
#?(:clj
(defn- format-clj-stacktrace
[trace]
(let [sb (enc/str-builder)
s+nl (enc/sb-appender sb enc/newline)]
(doseq [st-el (force trace)]
(let [{:keys [class method file line]} st-el]
(s+nl class "/" method " at " file ":" line)))
(str sb))))
(comment (println (format-clj-stacktrace (:trace (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})))))))
(defn format-error-fn
"Experimental, subject to change.
Returns a (fn format [error]) that:
@ -281,12 +293,9 @@
(s+ nl " data: " (enc/pr-edn* data)))))
(when trace
(s+ nl nl "Root stack trace:")
#?(:cljs (s+ nl trace)
:clj
(doseq [st-el (force trace)]
(let [{:keys [class method file line]} st-el]
(s+ nl "" class "/" method " at " file ":" line)))))
(s+ nl nl "Root stack trace:" nl)
#?(:cljs (s+ trace)
:clj (format-clj-stacktrace trace)))
(str sb)))))))

View file

@ -14,7 +14,8 @@
#?(:clj [clojure.tools.logging :as ctl])
#?(:default [taoensso.telemere.handlers.console :as handlers:console])
#?(:clj [taoensso.telemere.handlers.file :as handlers:file])))
#?(:clj [taoensso.telemere.handlers.file :as handlers:file])
#?(:clj [taoensso.telemere.handlers.open-telemetry :as handlers:otel])))
(comment
(remove-ns 'taoensso.telemere-tests)
@ -790,11 +791,99 @@
;;;; Other handlers
(deftest _other-handlers
;; For now just testing that basic construction succeeds
(deftest _handler-constructors
[#?(:default (is (fn? (handlers:console/handler:console))))
#?(:cljs (is (fn? (handlers:console/handler:console-raw))))
#?(:clj (is (fn? (handlers:file/handler:file))))])
#?(:clj (is (fn? (handlers:file/handler:file))))
#?(:clj (is (fn? (handlers:otel/handler:open-telemetry-logger))))])
(comment (def attrs-map handlers:otel/signal->attrs-map))
#?(:clj
(deftest _open-telemetry
[(testing "attr-name"
[(is (= (handlers:otel/attr-name :foo) "foo"))
(is (= (handlers:otel/attr-name :foo-bar-baz) "foo_bar_baz"))
(is (= (handlers:otel/attr-name :foo/bar-baz) "foo.bar_baz"))
(is (= (handlers:otel/attr-name :Foo/Bar-BAZ) "foo.bar_baz"))
(is (= (handlers:otel/attr-name "Foo Bar-Baz") "foo_bar_baz"))
(is (= (handlers:otel/attr-name :x1.x2/x3-x4 :foo/bar-baz)
"x1.x2.x3_x4.foo.bar_baz"))])
(testing "merge-prefix-map"
[(is (= (handlers:otel/merge-prefix-map nil "pf" nil) nil))
(is (= (handlers:otel/merge-prefix-map nil "pf" {}) nil))
(is (= (handlers:otel/merge-prefix-map {"a" "A"} "pf" {:a :A}) {"a" "A", "pf.a" :A}))
(is (= (handlers:otel/merge-prefix-map {} "pf"
{:a/b1 "v1" :a/b2 "v2" :nil nil, :map {:k1 "v1"}})
{"pf.a.b1" "v1", "pf.a.b2" "v2", "pf.nil" nil, "pf.map" {:k1 "v1"}}))])
(testing "as-attrs"
(is (= (str
(handlers:otel/as-attrs
{:string "s", :keyword :foo/bar, :long 5, :double 5.0, :nil nil,
:longs [5 5.0 5.0],
:doubles [5.0 5 5],
:bools [true false nil],
:mixed [5 "5" nil],
:strings ["a" "b" "c"],
:map {:k1 "v1"}}))
"{bools=[true, false, false], double=5.0, doubles=[5.0, 5.0, 5.0], keyword=\":foo/bar\", long=5, longs=[5, 5, 5], map=[[:k1 \"v1\"]], mixed=[5, \"5\", nil], nil=\"nil\", string=\"s\", strings=[\"a\", \"b\", \"c\"]}")))
(testing "signal->attrs-map"
(let [attrs-map handlers:otel/signal->attrs-map]
[(is (= (attrs-map nil { }) {"error" false}))
(is (= (attrs-map :attrs {:attrs {:a1 :A1}}) {"error" false, :a1 :A1}))
(is
(sm?
(attrs-map :attrs
{:ns "ns"
:line 100
:file "file"
:error ex2
:kind :event
:level :info
:id ::id1
:uid #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
:run-form '(+ 3 2)
:run-val 5
:run-nsecs 100
:sample-rate 0.5
:parent
{:id ::parent-id1
:uid #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"}
:attrs {:a1 :A1}})
{"ns" "ns"
"line" 100
"file" "file"
"error" true
"exception.type" 'clojure.lang.ExceptionInfo
"exception.message" "Ex1"
"exception.stacktrace" (enc/pred string?)
"exception.data.k1" "v1"
"kind" :event
"level" :info
"id" :taoensso.telemere-tests/id1
"parent.id" :taoensso.telemere-tests/parent-id1
"uid" #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
"parent.uid" #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"
"run.form" '(+ 3 2)
"run.val" 5
"run.val_type" 'java.lang.Long
"run.nsecs" 100
"sample" 0.5
:a1 :A1}))]))]))
;;;;