diff --git a/src/taoensso/telemere.cljc b/src/taoensso/telemere.cljc index de7992f..96fb84d 100644 --- a/src/taoensso/telemere.cljc +++ b/src/taoensso/telemere.cljc @@ -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 diff --git a/src/taoensso/telemere/handlers/open_telemetry.clj b/src/taoensso/telemere/handlers/open_telemetry.clj new file mode 100644 index 0000000..0918124 --- /dev/null +++ b/src/taoensso/telemere/handlers/open_telemetry.clj @@ -0,0 +1,216 @@ +(ns ^:no-doc taoensso.telemere.handlers.open-telemetry + "Private ns, implementation detail. + Core OpenTelemetry handlers. + + Needs `OpenTelemetry Java`, + Ref. ." + + (: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. " + (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. ." + [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))))))))) diff --git a/src/taoensso/telemere/utils.cljc b/src/taoensso/telemere/utils.cljc index 8b65fb6..cbe7ad5 100644 --- a/src/taoensso/telemere/utils.cljc +++ b/src/taoensso/telemere/utils.cljc @@ -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))))))) diff --git a/test/taoensso/telemere_tests.cljc b/test/taoensso/telemere_tests.cljc index 16fc958..fc23ce1 100644 --- a/test/taoensso/telemere_tests.cljc +++ b/test/taoensso/telemere_tests.cljc @@ -13,8 +13,9 @@ #?(:clj [taoensso.telemere.slf4j :as slf4j]) #?(:clj [clojure.tools.logging :as ctl]) - #?(:default [taoensso.telemere.handlers.console :as handlers:console]) - #?(:clj [taoensso.telemere.handlers.file :as handlers:file]))) + #?(:default [taoensso.telemere.handlers.console :as handlers:console]) + #?(: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}))]))])) ;;;;