diff --git a/.github/workflows/graal-tests.yml b/.github/workflows/graal-tests.yml new file mode 100644 index 0000000..14495f4 --- /dev/null +++ b/.github/workflows/graal-tests.yml @@ -0,0 +1,32 @@ +name: Graal tests +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + java: ['17'] + os: [ubuntu-latest, macOS-latest, windows-latest] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: graalvm/setup-graalvm@v1 + with: + version: 'latest' + java-version: ${{ matrix.java }} + components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: DeLaGuardo/setup-clojure@12.5 + with: + lein: latest + bb: latest + + - uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: deps-${{ hashFiles('deps.edn') }} + restore-keys: deps- + + - run: bb graal-tests diff --git a/.github/workflows/main-tests.yml b/.github/workflows/main-tests.yml new file mode 100644 index 0000000..faa64d6 --- /dev/null +++ b/.github/workflows/main-tests.yml @@ -0,0 +1,30 @@ +name: Main tests +on: [push, pull_request] + +jobs: + tests: + strategy: + matrix: + java: ['17', '18', '19'] + os: [ubuntu-latest] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: ${{ matrix.java }} + + - uses: DeLaGuardo/setup-clojure@12.5 + with: + lein: latest + + - uses: actions/cache@v4 + id: cache-deps + with: + path: ~/.m2/repository + key: deps-${{ hashFiles('project.clj') }} + restore-keys: deps- + + - run: lein test-all diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6c6c301 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning). + +--- diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 0000000..964e36a --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1,2 @@ +github: ptaoussanis +custom: "https://www.taoensso.com/clojure" diff --git a/README.md b/README.md index f0f241a..7040db8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,57 @@ -# telemere -Coming later +Taoensso open source +[**Documentation**](#documentation) | [Latest releases](#latest-releases) | [Get support][GitHub issues] + +# Telemere + +### Structured telemetry library for Clojure/Script + +This library is **still under development** and not available yet for public use. + +## Latest release/s + +- Coming [~Apr 2024](https://www.taoensso.com/roadmap) + +[![Main tests][Main tests SVG]][Main tests URL] +[![Graal tests][Graal tests SVG]][Graal tests URL] + +See [here][GitHub releases] for earlier releases. + +## Why Telemere? + +- Coming later + +## Documentation + +- [Wiki][GitHub wiki] (getting started, usage, etc.) +- API reference: [Codox][Codox docs], [clj-doc][clj-doc docs] + +## Funding + +You can [help support][sponsor] continued work on this project, thank you!! 🙏 + +## License + +Copyright © 2023-2024 [Peter Taoussanis][]. +Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). + + + +[GitHub releases]: ../../releases +[GitHub issues]: ../../issues +[GitHub wiki]: ../../wiki + +[Peter Taoussanis]: https://www.taoensso.com +[sponsor]: https://www.taoensso.com/sponsor + + + +[Codox docs]: https://taoensso.github.io/telemere/ +[clj-doc docs]: https://cljdoc.org/d/com.taoensso/telemere/ + +[Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/telemere.svg +[Clojars URL]: https://clojars.org/com.taoensso/telemere + +[Main tests SVG]: https://github.com/taoensso/telemere/actions/workflows/main-tests.yml/badge.svg +[Main tests URL]: https://github.com/taoensso/telemere/actions/workflows/main-tests.yml +[Graal tests SVG]: https://github.com/taoensso/telemere/actions/workflows/graal-tests.yml/badge.svg +[Graal tests URL]: https://github.com/taoensso/telemere/actions/workflows/graal-tests.yml diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f75c342 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +## Advisories + +All security advisories will be posted [on GitHub](https://github.com/taoensso/telemere/security/advisories). + +## Reporting a vulnerability + +Please report possible security vulnerabilities [via GitHub](https://github.com/taoensso/telemere/security/advisories), or by emailing me at `my first name at taoensso.com`. You may encrypt emails with [my public PGP/GPG key](https://www.taoensso.com/pgp). + +Thank you! + +\- [Peter Taoussanis](https://www.taoensso.com) diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..5721f2a --- /dev/null +++ b/bb.edn @@ -0,0 +1,10 @@ +{:paths ["bb"] + :tasks + {:requires ([graal-tests]) + graal-tests + {:doc "Run Graal native-image tests" + :task + (do + (graal-tests/uberjar) + (graal-tests/native-image) + (graal-tests/run-tests))}}} diff --git a/bb/graal_tests.clj b/bb/graal_tests.clj new file mode 100755 index 0000000..3397ebe --- /dev/null +++ b/bb/graal_tests.clj @@ -0,0 +1,38 @@ +#!/usr/bin/env bb + +(ns graal-tests + (:require + [clojure.string :as str] + [babashka.fs :as fs] + [babashka.process :refer [shell]])) + +(defn uberjar [] + (let [command "lein with-profiles +graal-tests uberjar" + command + (if (fs/windows?) + (if (fs/which "lein") + command + ;; Assume PowerShell powershell module + (str "powershell.exe -command " (pr-str command))) + command)] + + (shell command))) + +(defn executable [dir name] + (-> (fs/glob dir (if (fs/windows?) (str name ".{exe,bat,cmd}") name)) + first + fs/canonicalize + str)) + +(defn native-image [] + (let [graalvm-home (System/getenv "GRAALVM_HOME") + bin-dir (str (fs/file graalvm-home "bin"))] + (shell (executable bin-dir "gu") "install" "native-image") + (shell (executable bin-dir "native-image") + "--features=clj_easy.graal_build_time.InitClojureClasses" + "--no-fallback" "-jar" "target/graal-tests.jar" "graal_tests"))) + +(defn run-tests [] + (let [{:keys [out]} (shell {:out :string} (executable "." "graal_tests"))] + (assert (str/includes? out "loaded") out) + (println "Native image works!"))) diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn new file mode 100644 index 0000000..ae6d424 --- /dev/null +++ b/doc/cljdoc.edn @@ -0,0 +1,2 @@ +{:cljdoc/docstring-format :plaintext} + diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..e3c231b --- /dev/null +++ b/project.clj @@ -0,0 +1,86 @@ +(defproject com.taoensso/telemere "1.0.0-SNAPSHOT" + :author "Peter Taoussanis " + :description "Structured telemetry library for Clojure/Script" + :url "https://www.taoensso.com/telemere" + + :license + {:name "Eclipse Public License - v 1.0" + :url "https://www.eclipse.org/legal/epl-v10.html"} + + :dependencies + [[com.taoensso/encore "3.88.0"] + [org.clj-commons/pretty "2.2.1"]] + + :test-paths ["test" #_"src"] + + :profiles + {;; :default [:base :system :user :provided :dev] + :provided {:dependencies [[org.clojure/clojurescript "1.11.132"] + [org.clojure/clojure "1.11.1"]]} + :c1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]} + :c1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]} + :c1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} + + :graal-tests + {:source-paths ["test"] + :main taoensso.graal-tests + :aot [taoensso.graal-tests] + :uberjar-name "graal-tests.jar" + :dependencies + [[org.clojure/clojure "1.11.1"] + [com.github.clj-easy/graal-build-time "1.0.5"]]} + + :dev + {:jvm-opts + ["-server" + "-Dtaoensso.elide-deprecated=true" + "-Dclojure.tools.logging->telemere?=true"] + + :global-vars + {*warn-on-reflection* true + *assert* true + *unchecked-math* false #_:warn-on-boxed} + + :dependencies + [[org.clojure/test.check "1.1.1"] + [org.clojure/tools.logging "1.3.0"] + [org.slf4j/slf4j-api "2.0.12"] + [com.taoensso/slf4j-telemere "1.0.0-SNAPSHOT"] + ;; [org.slf4j/slf4j-simple "2.0.12"] + ;; [org.slf4j/slf4j-nop "2.0.12"] + [io.opentelemetry/opentelemetry-api "1.35.0"]] + + :plugins + [[lein-pprint "1.3.2"] + [lein-ancient "0.7.0"] + [lein-cljsbuild "1.1.8"] + [com.taoensso.forks/lein-codox "0.10.11"]] + + :codox + {:language #{:clojure :clojurescript} + :base-language :clojure}}} + + :cljsbuild + {:test-commands {"node" ["node" "target/test.js"]} + :builds + [{:id :main + :source-paths ["src"] + :compiler + {:output-to "target/main.js" + :optimizations :advanced}} + + {:id :test + :source-paths ["src" "test"] + :compiler + {:output-to "target/test.js" + :target :nodejs + :optimizations :simple}}]} + + :aliases + {"start-dev" ["with-profile" "+dev" "repl" ":headless"] + "build-once" ["do" ["clean"] ["cljsbuild" "once"]] + "deploy-lib" ["do" ["build-once"] ["deploy" "clojars"] ["install"]] + + "test-clj" ["with-profile" "+c1.11:+c1.10:+c1.9" "test"] + "test-cljs" ["with-profile" "+test" "cljsbuild" "test"] + "test-all" ["do" ["clean"] ["test-clj"] ["test-cljs"]]}) diff --git a/slf4j/.gitignore b/slf4j/.gitignore new file mode 100644 index 0000000..371aff1 --- /dev/null +++ b/slf4j/.gitignore @@ -0,0 +1,16 @@ +pom.xml* +.lein* +.nrepl-port +*.jar +*.class +.env +.DS_Store +/lib/ +/classes/ +/target/ +/checkouts/ +/logs/ +/.clj-kondo/.cache +.idea/ +*.iml +/wiki/.git diff --git a/slf4j/project.clj b/slf4j/project.clj new file mode 100644 index 0000000..72e867b --- /dev/null +++ b/slf4j/project.clj @@ -0,0 +1,22 @@ +(defproject com.taoensso/slf4j-telemere "1.0.0-SNAPSHOT" + :author "Peter Taoussanis " + :description "Telemere backend/provider for SLF4J API v2" + :url "https://www.taoensso.com/telemere" + + :license + {:name "Eclipse Public License - v 1.0" + :url "https://www.eclipse.org/legal/epl-v10.html"} + + :java-source-paths ["src/java"] + :javac-options ["--release" "11" "-g"] ; Support Java >= v11 + :dependencies [] + + :profiles + {:provided + {:dependencies + [[org.clojure/clojure "1.11.1"] + [org.slf4j/slf4j-api "2.0.12"] + [com.taoensso/telemere "1.0.0-SNAPSHOT"]]}} + + :aliases + {"deploy-lib" ["do" #_["build-once"] ["deploy" "clojars"] ["install"]]}) diff --git a/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100755 index 0000000..47bcb1b --- /dev/null +++ b/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +com.taoensso.telemere.slf4j.TelemereServiceProvider \ No newline at end of file diff --git a/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLogger.java b/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLogger.java new file mode 100644 index 0000000..44d92be --- /dev/null +++ b/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLogger.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.taoensso.telemere.slf4j; +// Based on `org.slf4j.simple.SimpleLogger` + +import java.io.Serializable; + +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; +import org.slf4j.helpers.LegacyAbstractLogger; +import org.slf4j.spi.LoggingEventAware; + +import clojure.java.api.Clojure; +import clojure.lang.IFn; + +public class TelemereLogger extends LegacyAbstractLogger implements LoggingEventAware, Serializable { + + private static final long serialVersionUID = -1999356203037132557L; + + private static boolean INITIALIZED = false; + static void lazyInit() { + if (INITIALIZED) { return; } + INITIALIZED = true; + init(); + } + + private static IFn logFn; + private static IFn isLevelEnabledFn; + + static void init() { + IFn requireFn = Clojure.var("clojure.core", "require"); + requireFn.invoke( Clojure.read("taoensso.telemere.slf4j")); + logFn = Clojure.var("taoensso.telemere.slf4j", "log!"); + isLevelEnabledFn = Clojure.var("taoensso.telemere.slf4j", "allowed?"); + } + + protected TelemereLogger(String name) { this.name = name; } + + protected boolean isLevelEnabled(Level level) { return (boolean) isLevelEnabledFn.invoke(level); } + public boolean isTraceEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.TRACE); } + public boolean isDebugEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.DEBUG); } + public boolean isInfoEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.INFO); } + public boolean isWarnEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.WARN); } + public boolean isErrorEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.ERROR); } + + public void log(LoggingEvent event) { logFn.invoke(event); } // Fluent (modern) API, called after level check + + @Override protected String getFullyQualifiedCallerName() { return null; } + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) { + logFn.invoke(level, throwable, messagePattern, arguments, marker); // Legacy API, called after level check + } + +} diff --git a/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLoggerFactory.java b/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLoggerFactory.java new file mode 100644 index 0000000..55f63e8 --- /dev/null +++ b/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLoggerFactory.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.taoensso.telemere.slf4j; +// Based on `org.slf4j.simple.SimpleLoggerFactory` + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.slf4j.Logger; +import org.slf4j.ILoggerFactory; + +public class TelemereLoggerFactory implements ILoggerFactory { + + ConcurrentMap loggerMap; + + public TelemereLoggerFactory() { + loggerMap = new ConcurrentHashMap<>(); + TelemereLogger.lazyInit(); + } + + public Logger getLogger(String name) { + return loggerMap.computeIfAbsent(name, this::createLogger); + } + + protected Logger createLogger(String name) { + return new TelemereLogger(name); + } + + protected void reset() { + loggerMap.clear(); + } +} diff --git a/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereServiceProvider.java b/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereServiceProvider.java new file mode 100755 index 0000000..d88eff5 --- /dev/null +++ b/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereServiceProvider.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.taoensso.telemere.slf4j; +// Based on `org.slf4j.simple.SimpleServiceProvider` + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.helpers.BasicMDCAdapter; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +public class TelemereServiceProvider implements SLF4JServiceProvider { + + public static String REQUESTED_API_VERSION = "2.0.99"; // Should not be final + + private ILoggerFactory loggerFactory; + private IMarkerFactory markerFactory; + private MDCAdapter mdcAdapter; + + public ILoggerFactory getLoggerFactory() { return loggerFactory; } + @Override public IMarkerFactory getMarkerFactory() { return markerFactory; } + @Override public MDCAdapter getMDCAdapter() { return mdcAdapter; } + @Override public String getRequestedApiVersion() { return REQUESTED_API_VERSION; } + @Override + public void initialize() { + loggerFactory = new TelemereLoggerFactory(); + markerFactory = new BasicMarkerFactory(); + mdcAdapter = new BasicMDCAdapter(); + } + +} diff --git a/slf4j/src/taoensso/telemere/slf4j.clj b/slf4j/src/taoensso/telemere/slf4j.clj new file mode 100644 index 0000000..ddac9f9 --- /dev/null +++ b/slf4j/src/taoensso/telemere/slf4j.clj @@ -0,0 +1,175 @@ +(ns ^:no-doc taoensso.telemere.slf4j + "Private ns, implementation detail. + Interop support: SLF4J API v2 -> Telemere. + + To use Telemere as your SLF4J backend/provider, just include the + `com.taoensso/slf4j-telemere` dependency on your classpath. + + Implementation details, + Ref. : + + - Libs must include `org.slf4j/slf4j-api` dependency, but NO backend. + + - Users must include a single backend dependency of their choice + (e.g. `com.taoensso/slf4j-telemere` or `org.slf4j/slf4j-simple`). + + - SLF4J uses standard `ServiceLoader` mechanism to find its logging backend, + searches for `SLF4JServiceProvider` provider on classpath." + + (:require + [taoensso.encore :as enc :refer [have have?]] + [taoensso.telemere.impl :as impl])) + +;;;; Utils + +(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body))) + +(defn- sig-level + "Returns `taoensso.encore.signals` level for given `org.slf4j.event.Level`." + ;; Faster than switching on `org.slf4j.event.EventConstants` directly + [^org.slf4j.event.Level level] + (enc/case-eval (.toInt level) + org.slf4j.event.EventConstants/TRACE_INT :trace + org.slf4j.event.EventConstants/DEBUG_INT :debug + org.slf4j.event.EventConstants/INFO_INT :info + org.slf4j.event.EventConstants/WARN_INT :warn + org.slf4j.event.EventConstants/ERROR_INT :error + (throw + (ex-info "Unexpected `org.slf4j.event.Level`" + {:level {:value level, :type (type level)}})))) + +(comment (enc/qb 1e6 (sig-level org.slf4j.event.Level/INFO))) ; 36.47 + +(defn get-marker "Private util for tests, etc." + ^org.slf4j.Marker [n] (org.slf4j.MarkerFactory/getMarker n)) + +(defn est-marker! + "Private util for tests, etc. + Globally establishes (compound) `org.slf4j.Marker` with name `n` and mutates it + (all occurences!) to have exactly the given references. Returns the (compound) marker." + ^org.slf4j.Marker [n & refs] + (let [m (get-marker n)] + (enc/reduce-iterator! (fn [_ in] (.remove m in)) nil (.iterator m)) + (doseq [n refs] (.add m (get-marker n))) + m)) + +(comment [(est-marker! "a1" "a2") (get-marker "a1") (= (get-marker "a1") (get-marker "a1"))]) + +(def marker-names + "Returns #{}. Cached => assumes markers NOT modified after creation." + ;; We use `BasicMarkerFactory` so: + ;; 1. Our markers are just labels (no other content besides their name). + ;; 2. Markers with the same name are identical (enabling caching). + (enc/fmemoize + (fn marker-names [marker-or-markers] + (if (instance? org.slf4j.Marker marker-or-markers) + + ;; Single marker + (let [^org.slf4j.Marker m marker-or-markers + acc #{(.getName m)}] + + (if-not (.hasReferences m) + acc + (enc/reduce-iterator! + (fn [acc ^org.slf4j.Marker in] + (if-not (.hasReferences in) + (conj acc (.getName in)) + (into acc (marker-names in)))) + acc (.iterator m)))) + + ;; Vector of markers + (reduce + (fn [acc in] (into acc (marker-names in))) + #{} (have vector? marker-or-markers)))))) + +(comment + (let [m1 (est-marker! "M1") + m2 (est-marker! "M1") + cm (est-marker! "Compound" "M1" "M2") + ms [m1 m2]] + + (enc/qb 1e6 ; [45.52 47.48 44.85] + (marker-names m1) + (marker-names cm) + (marker-names ms)))) + +;;;; Interop fns (called by `TelemereLogger`) + +(defn allowed? + "Private, don't use. + Called by `com.taoensso.telemere.slf4j.TelemereLogger`." + [^org.slf4j.event.Level level] + (when-debug (println [:slf4j/allowed? (sig-level level)])) + (impl/signal-allowed? + {:location nil + :ns nil + :kind :log + :id :taoensso.telemere/slf4j + :level (sig-level level)})) + +(defn- normalized-log! + [instant level error msg-pattern args marker-names kvs] + (when-debug (println [:slf4j/normalized-log! (sig-level level)])) + (impl/signal! + {:allow? true ; Pre-filtered by `allowed?` call + :location nil + :ns nil + :kind :log + :id :taoensso.telemere/slf4j + :level (sig-level level) + :instant instant + :error error + + :ctx + (when-let [hmap (org.slf4j.MDC/getCopyOfContextMap)] + (clojure.lang.PersistentHashMap/create hmap)) + + :msg + (delay + (org.slf4j.helpers.MessageFormatter/basicArrayFormat + msg-pattern args)) + + :data + (enc/assoc-some nil + :slf4j/marker-names marker-names + :slf4j/args (when args (vec args)) + :slf4j/kvs kvs)}) + nil) + +(defn log! + "Private, don't use. + Called by `com.taoensso.telemere.slf4j.TelemereLogger`." + + ;; Modern "fluent" API calls + ([^org.slf4j.event.LoggingEvent event] + (let [instant (when-let [ts (.getTimeStamp event)] (when-not (zero? ts) (java.time.Instant/ofEpochMilli ts))) + level (.getLevel event) + error (.getThrowable event) + msg-pattern (.getMessage event) + args (when-let [args (.getArgumentArray event)] args) + markers (when-let [markers (.getMarkers event)] (marker-names (vec markers))) + kvs (when-let [kvps (.getKeyValuePairs event)] + (reduce + (fn [acc ^org.slf4j.event.KeyValuePair kvp] + (assoc acc (.-key kvp) (.-value kvp))) + nil kvps))] + + (when-debug (println [:slf4j/fluent-log-call (sig-level level)])) + (normalized-log! instant level error msg-pattern args markers kvs))) + + ;; Legacy API calls + ([^org.slf4j.event.Level level error msg-pattern args marker] + (let [marker-names (when marker (marker-names marker))] + (when-debug (println [:slf4j/legacy-log-call (sig-level level)])) + (normalized-log! :auto level error msg-pattern args marker-names nil)))) + +(comment + (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MySlfLogger")) + (impl/with-signal (-> sl (.info "Hello {}" "x"))) + (impl/with-signal (-> (.atInfo sl) (.log "Hello {}" "x"))) + + (do ; Will noop with `NOPMDCAdapter` + (org.slf4j.MDC/put "key" "val") + (org.slf4j.MDC/get "key") + (org.slf4j.MDC/getCopyOfContextMap) + (org.slf4j.MDC/clear))) diff --git a/src/taoensso/telemere.cljc b/src/taoensso/telemere.cljc new file mode 100644 index 0000000..d8fe1de --- /dev/null +++ b/src/taoensso/telemere.cljc @@ -0,0 +1,431 @@ +(ns taoensso.telemere + "Structured telemetry for Clojure/Script applications. + + See the GitHub page (esp. Wiki) for info on motivation and design: + " + + {:author "Peter Taoussanis (@ptaoussanis)"} + (:refer-clojure :exclude [newline]) + (:require + [taoensso.encore :as enc :refer [have have?]] + [taoensso.encore.signals :as sigs] + [taoensso.telemere.impl :as impl] + + #?(:clj [clj-commons.format.exceptions :as fmt-ex]) + #?(:clj [clj-commons.ansi :as fmt-ansi]))) + +(comment + (remove-ns 'taoensso.telemere) + (:api (enc/interns-overview))) + +(enc/assert-min-encore-version [3 88 0]) + +;;;; Roadmap +;; x Fundamentals +;; x Basic logging utils +;; x Interop: SLF4J +;; x Interop: `clojure.tools.logging` +;; - Core logging handlers +;; - First docs, intro video +;; - First OpenTelemetry tools +;; - Update Tufte (signal API, config API, signal fields, etc.) +;; - Update Timbre (signal API, config API, signal fields, backport improvements) + +;;;; TODO +;; - `clojure.tools.logging/log-capture!`, `with-logs`, etc. +;; - Via Timbre: core handlers, any last utils? +;; - Cljs (.log js/console ) better than string stacktrace (clickable, etc.) +;; +;; - Tests for utils (hostname, formatters, etc.)? +;; - Remaining docstrings and TODOs +;; - Document kinds: #{:log :spy :trace :event :error } +;; - General polish +;; +;; - Reading plan +;; - Recheck `ensso/telemere-draft.cljc` +;; - Cleanup `ensso/telemere-drafts.txt` +;; +;; - Decide on module/import/alias/project approach +;; - Initial README, wiki docs, etc. +;; - Explainer/demo video + +;;;; Shared signal API + +(sigs/def-api + {:purpose "signal" + :sf-arity 4 + :*sig-handlers* impl/*sig-handlers* + :*rt-sig-filter* impl/*rt-sig-filter*}) + +(comment + [level-aliases] + [handlers-help get-handlers add-handler! remove-handler! with-handler with-handler+] + [filtering-help get-filters get-min-level + set-kind-filter! set-ns-filter! set-id-filter! set-min-level! + with-kind-filter with-ns-filter with-id-filter with-min-level]) + +;;;; Aliases + +(enc/defaliases + #?(:clj enc/set-var-root!) + #?(:clj enc/update-var-root!) + #?(:clj enc/get-env) + enc/chance + enc/rate-limiter + enc/newline + impl/msg-splice + impl/msg-skip + #?(:clj impl/with-signal) + #?(:clj impl/signal!)) + +;;;; Context + +(enc/defonce default-ctx + "Advanced feature. Default root (base) value of `*ctx*` var, controlled by: + (get-env {:as :edn} :taoensso.telemere/default-ctx<.platform><.edn>) + + See `get-env` for details." + (enc/get-env {:as :edn} :taoensso.telemere/default-ctx<.platform><.edn>)) + +(enc/def* ^:dynamic *ctx* + "Dynamic context: arbitrary app-level state attached as `:ctx` to all signals. + Value may be any type, but is usually nil or a map. + + Re/bind dynamic value using `with-ctx`, `with-ctx+`, or `binding`. + Modify root (base) value using `set-ctx!`. + Default root (base) value is `default-ctx`. + + Note that as with all dynamic Clojure vars, \"binding conveyance\" applies + when using futures, agents, etc. + + Tips: + - Value may be (or may contain) an atom if you want mutable semantics + - Value may be of form { } for custom scoping, etc." + default-ctx) + +#?(:clj + (defmacro set-ctx! + "Set `*ctx*` var's root (base) value. See `*ctx*` for details." + [root-val] `(enc/set-var-root! *ctx* ~root-val))) + +#?(:clj + (defmacro with-ctx + "Evaluates given form with given `*ctx*` value. See `*ctx*` for details." + [init-val form] `(binding [*ctx* ~init-val] ~form))) + +(comment (with-ctx "my-ctx" *ctx*)) + +#?(:clj + (defmacro with-ctx+ + "Evaluates given form with updated `*ctx*` value. + + `update-map-or-fn` may be: + - A map to merge with current `*ctx*` value, or + - A unary fn to apply to current `*ctx*` value + + See `*ctx*` for details." + [update-map-or-fn form] + `(binding [*ctx* (impl/update-ctx *ctx* ~update-map-or-fn)] + ~form))) + +(comment (with-ctx {:a :A1 :b :B1} (with-ctx+ {:a :A2} *ctx*))) + +;;;; Middleware + +(enc/defonce ^:dynamic *middleware* + "Optional vector of unary middleware fns to apply (sequentially/left-to-right) + to each signal before passing it to handlers. If any middleware fn returns nil, + aborts immediately without calling handlers. + + Useful for transforming each signal before handling. + + Re/bind dynamic value using `with-middleware`, `binding`. + Modify root (base) value using `set-middleware!`." + nil) + +#?(:clj + (defmacro set-middleware! + "Set `*middleware*` var's root (base) value. See `*middleware*` for details." + [root-val] `(enc/set-var-root! *middleware* ~root-val))) + +#?(:clj + (defmacro with-middleware + "Evaluates given form with given `*middleware*` value. + See `*middleware*` for details." + [init-val form] `(binding [*middleware* ~init-val] ~form))) + +;;;; Encore integration + +(do + (enc/set-var-root! sigs/*default-handler-error-fn* + (fn [{:keys [error] :as m}] + (impl/signal! + {:level :error + :error error + :location {:ns "taoensso.encore.signals"} + :id :taoensso.encore.signals/handler-error + :msg "[encore/signals] Error executing wrapped handler fn" + :data (dissoc m :error)}))) + + (enc/set-var-root! sigs/*default-handler-backp-fn* + (fn [data] + (impl/signal! + {:level :warn + :location {:ns "taoensso.encore.signals"} + :id :taoensso.encore.signals/handler-back-pressure + :msg "[encore/signals] Back pressure on wrapped handler fn" + :data data})))) + +;;;; Common signals +;; - log! [msg] [level-or-opts msg] ; msg + ?level => allowed? +;; - event! [id] [level-or-opts id] ; id + ?level => allowed? +;; - error! [error] [id-or-opts error] ; error + ?id => error +;; - trace! [form] [id-or-opts form] ; run + ?id => run result (value or throw) +;; - spy! [form] [level-or-opts form] ; run + ?level => run result (value or throw) +;; - catch->error! [form] [id-or-opts form] ; run + ?id => run value or ?return +;; - uncaught->error! [] [id-or-opts ] ; ?id => nil + +#?(:clj + (defmacro log! + "TODO Docstring [msg] [level-or-opts msg] => allowed?" + {:arglists (impl/signal-arglists :log!)} + [& args] + (let [opts (apply impl/signal-opts :msg, :level, {:kind :log, :level :info} args)] + (enc/keep-callsite `(impl/signal! ~opts))))) + +#?(:clj + (defmacro event! + "TODO Docstring [id] [level-or-opts id] => allowed?" + {:arglists (impl/signal-arglists :event!)} + [& args] + (let [opts (apply impl/signal-opts :id, :level, {:kind :event, :level :info} args)] + (enc/keep-callsite `(impl/signal! ~opts))))) + +#?(:clj + (defmacro error! + "TODO Docstring [error] [id-or-opts error] => error + (throw (error! )) example." + {:arglists (impl/signal-arglists :error!)} + [& args] + (let [opts (apply impl/signal-opts :error, :id, {:kind :error, :level :error} args) + error-form (get opts :error)] + ;; (enc/keep-callsite `(impl/signal! ~opts)) ; => allowed? + (enc/keep-callsite + `(let [~'__error ~error-form] + (impl/signal! ~(assoc opts :error '__error)) + ~'__error))))) + +(comment (throw (error! (Exception. "hello")))) + +#?(:clj + (defmacro trace! + "TODO Docstring [form] [id-or-opts form] => run result (value or throw)" + {:arglists (impl/signal-arglists :trace!)} + [& args] + (let [opts (apply impl/signal-opts :run, :id, {:kind :trace, :level :info} args)] + (enc/keep-callsite `(impl/signal! ~opts))))) + +#?(:clj + (defmacro spy! + "TODO Docstring [form] [level-or-opts form] => run result (value or throw)" + {:arglists (impl/signal-arglists :spy!)} + [& args] + (let [opts (apply impl/signal-opts :run, :level, {:kind :spy, :level :info, :msg ::impl/spy} args)] + (enc/keep-callsite `(impl/signal! ~opts))))) + +#?(:clj + (defmacro catch->error! + "TODO Docstring [form] [id-or-opts form] => run value or ?catch-val" + {:arglists (impl/signal-arglists :catch->error!)} + [& args] + (let [opts (apply impl/signal-opts ::__form, :id, {:kind :error, :level :error} args) + rethrow? (if (contains? opts :catch-val) false (get opts :rethrow?)) + catch-val (get opts :catch-val) + form (get opts ::__form) + opts (dissoc opts ::__form :catch-val :rethrow?)] + + (enc/keep-callsite + `(enc/try* ~form + (catch :any ~'__t + (impl/signal! ~(assoc opts :error '__t)) + (if ~rethrow? (throw ~'__t) ~catch-val))))))) + +(comment (catch->error! {:id :id1, :catch-val "threw"} (/ 1 0))) + +#?(:clj + (defmacro uncaught->error! + "TODO Docstring + See also `uncaught->handler!`." + {:arglists (impl/signal-arglists :uncaught->error!)} + ([ ] (enc/keep-callsite `(uncaught->error! nil))) + ([id-or-opts] + (let [msg-form ["Uncaught Throwable on thread: " `(.getName ~(with-meta '__thread {:tag 'java.lang.Thread}))] + opts (impl/signal-opts :error, :id, {:kind :error, :level :error, :msg msg-form} id-or-opts '__throwable)] + + (enc/keep-callsite + `(uncaught->handler! + (fn [~'__thread ~'__throwable] + (impl/signal! ~opts)))))))) + +(comment (macroexpand '(uncaught->error! :id1))) + +;;;; Utils + +#?(:clj + (defn uncaught->handler! + "Sets JVM's global `DefaultUncaughtExceptionHandler` to given + (fn handler [`` ``]). + See also `uncaught->error!`." + [handler] + (Thread/setDefaultUncaughtExceptionHandler + (reify Thread$UncaughtExceptionHandler + (uncaughtException [_ thread throwable] + (handler thread throwable)))))) + +#?(:clj + (defn hostname + "Returns local cached hostname string, or `timeout-val` (default \"UnknownHost\")." + (^String [ ] (enc/get-hostname (enc/msecs :mins 1) 5000 "UnknownHost")) + ( [timeout-msecs timeout-val] (enc/get-hostname (enc/msecs :mins 1) timeout-msecs timeout-val)))) + +(comment (enc/qb 1e6 (hostname))) ; 76.64 + +#?(:clj (defn thread-name ^String [] (.getName (Thread/currentThread)))) +#?(:clj (defn thread-id ^String [] (.getId (Thread/currentThread)))) + +(defn format-instant + "TODO Docstring" + {:tag #?(:clj 'String :cljs 'string)} + ([instant] (format-instant nil instant)) + + #?(:cljs + ([{:keys [format]} instant] + (if format ; `goog.i18n.DateTimeFormat` + (.format format instant) + (.toISOString instant))) + + :clj + ([{:keys [formatter] + :or {formatter java.time.format.DateTimeFormatter/ISO_INSTANT}} + instant] + (.format + ^java.time.format.DateTimeFormatter formatter + ^java.time.Instant instant)))) + +(comment (format-instant (enc/now-inst))) + +(defn format-error + "TODO Docstring" + {:tag #?(:clj 'String :cljs 'string)} + ([error] (format-error nil error)) + + #?(:cljs + ([_ error] + (let [nl newline] + (str + (or + (.-stack error) ; Incl. `ex-message` content + (ex-message error)) + (when-let [data (ex-data error)] (str nl "ex-data:" nl " " (pr-str data))) + (when-let [cause (ex-cause error)] (str nl nl "Caused by:" nl (format-error cause)))))) + + :clj + ([{:keys [fonts sort] + :or + {fonts clj-commons.format.exceptions/default-fonts + sort :chronological #_:depth-first}} + error] + + (binding [fmt-ansi/*color-enabled* (not (empty? fonts)) + fmt-ex/*fonts* fonts + fmt-ex/*traditional* + (case sort + :depth-first true ; Traditional + :chronological false ; Modern (default) + (enc/unexpected-arg! sort + {:context `format-error + :param 'sort + :expected #{:depth-first :chronological}}))] + + (fmt-ex/format-exception error))))) + +(comment (println (format-error (ex-info "Ex2" {:k2 :v2} (ex-info "Ex1" {:k1 :v1}))))) + +;;;; Interop (`clojure.tools.logging`, SLF4J) + +#?(:clj + (def ^:private have-tools-logging? + (enc/compile-if + (do (require '[taoensso.telemere.tools-logging :as ttl]) true) + true false))) + +#?(:clj + (enc/compile-when have-tools-logging? + (enc/defalias ttl/tools-logging->telemere!) + (when (enc/get-env {:as :bool} :clojure.tools.logging->telemere?) + (ttl/tools-logging->telemere!)))) + +#?(:clj + (defn- interop-test! [msg form-fn] + (let [msg (str msg " (" (enc/uuid-str) ")") + signal + (binding [impl/*rt-sig-filter* nil] + (impl/with-signal {:stop-propagation? true, :return :signal} + (form-fn msg)))] + + (= (force (get signal :msg_)) msg)))) + +#?(:clj + (defn interop-check + "Tests Telemere's interop with `clojure.tools.logging` and SLF4J. + Returns {:keys [tools-logging slf4j]} with sub-maps: + {:keys [present? set->telemere? receiving?]}." + [] + (let [base-present {:present? true, :send->telemere? false, :receiving? false}] + {:tools-logging + (if-not (enc/have-resource? "clojure/tools/logging.clj") + {:present? false} + (merge base-present + (enc/compile-when have-tools-logging? + {:send->telemere? (ttl/tools-logging->telemere?) + :receiving? + (interop-test! + "Interop test: `clojure.tools.logging`->Telemere" + (fn [msg] (clojure.tools.logging/info msg)))}))) + + :slf4j + (if-not (enc/have-class? "org.slf4j.Logger") + {:present? false} + (merge base-present + (enc/compile-when + (and org.slf4j.Logger com.taoensso.telemere.slf4j.TelemereLogger) + (let [^org.slf4j.Logger sl + (org.slf4j.LoggerFactory/getLogger "InteropTestTelemereLogger")] + + {:send->telemere? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl) + :receiving? (interop-test! "Interop test: SLF4J->Telemere" (fn [msg] (.info sl msg)))}))))}))) + +(comment (check-interop)) + +;;;; Flow benchmarks + +(comment + {:last-updated "2024-02-12" + :system "2020 Macbook Pro M1, 16 GB memory" + :clojure-version "1.11.1" + :java-version "OpenJDK 21"} + + (binding [impl/*sig-handlers* nil] + + [(enc/qb 1e6 ; [9.26 16.85 187.3 202.7] + (signal! {:level :info, :run nil, :elide? true}) + (signal! {:level :info, :run nil, :allow? false}) + (signal! {:level :info, :run nil, :allow? true }) + (signal! {:level :info, :run nil})) + + (enc/qb 1e6 ; [8.09 15.29 677.91 278.57 688.89] + (signal! {:level :info, :run "run", :elide? true}) + (signal! {:level :info, :run "run", :allow? false}) + (signal! {:level :info, :run "run", :allow? true }) + (signal! {:level :info, :run "run", :trace? false}) + (signal! {:level :info, :run "run"}))])) diff --git a/src/taoensso/telemere/impl.cljc b/src/taoensso/telemere/impl.cljc new file mode 100644 index 0000000..8e452d4 --- /dev/null +++ b/src/taoensso/telemere/impl.cljc @@ -0,0 +1,541 @@ +(ns ^:no-doc taoensso.telemere.impl + "Private ns, implementation detail. + Signal design shared by: Telemere, Tufte, Timbre." + (:require + [taoensso.encore :as enc :refer [have have?]] + [taoensso.encore.signals :as sigs])) + +(comment + (remove-ns 'taoensso.telemere.impl) + (:api (enc/interns-overview))) + +;;;; Utils + +#?(:clj (defmacro threaded [& body] `(let [t# (Thread. (fn [] ~@body))] (.start t#) t#))) + +;;;; Config + +#?(:clj + (let [base (enc/get-env {:as :edn} :taoensso.telemere/ct-filters<.platform><.edn>) + ns-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-ns-filter<.platform><.edn>) + id-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-id-filter<.platform><.edn>) + min-level (enc/get-env {:as :edn} :taoensso.telemere/ct-min-level<.platform><.edn>)] + + (enc/defonce ct-sig-filter + "`SigFilter` used for compile-time elision, or nil." + (sigs/sig-filter + {:ns-filter (or ns-filter (get base :ns-filter)) + :id-filter (or id-filter (get base :id-filter)) + :min-level (or min-level (get base :min-level))})))) + +(let [base (enc/get-env {:as :edn} :taoensso.telemere/rt-filters<.platform><.edn>) + ns-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-ns-filter<.platform><.edn>) + id-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-id-filter<.platform><.edn>) + min-level (enc/get-env {:as :edn} :taoensso.telemere/rt-min-level<.platform><.edn>)] + + (enc/defonce ^:dynamic *rt-sig-filter* + "`SigFilter` used for runtime filtering, or nil." + (sigs/sig-filter + {:ns-filter (or ns-filter (get base :ns-filter)) + :id-filter (or id-filter (get base :id-filter)) + :min-level (or min-level (get base :min-level))}))) + +;;;; Context (optional arb app-level state) +;; taoensso.telemere/*ctx* + +(defn update-ctx + "Returns `new-ctx` given `old-ctx` and an update map or fn." + [old-ctx update-map-or-fn] + (enc/cond + (nil? update-map-or-fn) old-ctx + (map? update-map-or-fn) (enc/fast-merge old-ctx update-map-or-fn) ; Before ifn + (ifn? update-map-or-fn) (update-map-or-fn old-ctx) + :else + (enc/unexpected-arg! update-map-or-fn + {:context `update-ctx + :param 'update-map-or-fn + :expected '#{nil map fn}}))) + +;;;; Unique IDs (UIDs) + +(enc/def* nanoid-readable (enc/rand-id-fn {:chars :nanoid-readable, :len 23})) + +#?(:clj + (defn- parse-uid-form [uid-form] + (when uid-form + (case uid-form + :auto/uuid `(enc/uuid) + :auto/uuid-str `(enc/uuid-str) + :auto/nanoid `(enc/nanoid) + :auto/nanoid-readable `(nanoid-readable) + uid-form)))) + +(comment + (enc/qb 1e6 ; [164.72 184.51 301.8 539.49] + (enc/uuid) + (enc/uuid-str) + (enc/nanoid) + (nanoid-readable))) + +;;;; Messages + +(deftype MsgSplice [args]) +(deftype MsgSkip []) + +(defn ^:public msg-splice "TODO Docstring" [args] (MsgSplice. args)) +(defn ^:public msg-skip "TODO Docstring" [] (MsgSkip.)) + +(let [;; xform (map #(if (nil? %) "nil" %)) + xform + (fn [rf] + (let [;; Protocol-based impln (extensible but ~20% slower) + ;; rf* (fn rf* [acc in] (reduce-msg-arg in acc rf)) + rf* + (fn rf* [acc in] + (enc/cond + (instance? MsgSplice in) (reduce rf* acc (.-args ^MsgSplice in)) + (instance? MsgSkip in) acc + (nil? in) (rf acc "nil") + :else (rf acc in)))] + (fn + ([ ] (rf)) + ([acc ] (rf acc)) + ([acc in] (rf* acc in)))))] + + (defn signal-msg + "Returns string formed by joining all args without separator, + rendering nils as \"nil\"." + {:tag #?(:clj 'String :cljs 'string)} + [args] (enc/str-join nil xform args))) + +(comment + (enc/qb 2e6 ; [280.29 408.3] + (str "a" "b" "c" nil :kw) + (signal-msg ["a" "b" "c" nil :kw (msg-splice ["d" "e"])]))) + +#?(:clj + (defn- parse-msg-form [msg-form] + (when msg-form + (enc/cond + (string? msg-form) msg-form + (vector? msg-form) + (enc/cond + (empty? msg-form) nil + :let [[m1 & more] msg-form] + (and (string? m1) (nil? more)) m1 + :else `(delay (signal-msg ~msg-form))) + + ;; Auto delay-wrap (user should never delay-wrap!) + ;; :else `(delay ~msg-form) + + ;; Leave user to delay-wrap when appropriate (document) + :else msg-form)))) + +;;;; Tracing (optional flow tracking) + +(enc/def* ^:dynamic *trace-parent* "?TraceParent" nil) +(defrecord TraceParent [id uid]) + +#?(:clj + (defmacro with-tracing + "Wraps `form` with tracing iff const boolean `trace?` is true." + [trace? id uid form] + + ;; Not much motivation to support runtime `trace?` form, but easy + ;; to add support later if desired + (when-not (enc/const-form? trace?) + (enc/unexpected-arg! trace? + {:msg "Expected constant (compile-time) `:trace?` value" + :context `with-tracing})) + + (if trace? + `(binding [*trace-parent* (TraceParent. ~id ~uid)] ~form) + (do form)))) + +(comment + [(macroexpand '(with-tracing false :id1 :uid1 "form")) + (macroexpand '(with-tracing true :id1 :uid1 "form"))]) + +;;;; Main types + +(defrecord Signal + ;; Telemere's main public data type, we avoid field nesting and duplication + [^long schema-version instant uid, + callsite-id location ns line column file, + sample-rate, kind id level, ctx parent, + data msg_ error run-form run-value, + end-instant runtime-nsecs]) + +(deftype #_defrecord WrappedSignal + ;; Internal type to implement `sigs/IFilterableSignal`, + ;; incl. lazy + cached `signal-value_` field. + [ns kind id level signal-value_] + sigs/IFilterableSignal + (allow-signal? [_ sig-filter] (sig-filter ns kind id level)) + (signal-value [_ handler-context] + (let [sig-val @signal-value_] + (or + (when-let [handler-sample-rate + (when-let [^taoensso.encore.signals.HandlerContext hc handler-context] + (.-sample-rate hc))] + + (when (map? sig-val) + ;; Replace call sample rate with combined (call * handler) sample rate + (assoc sig-val :sample-rate + (* + (double handler-sample-rate) + (double (or (get sig-val :sample-rate) 1.0)))))) + sig-val)))) + +;;;; Handlers + +(enc/defonce ^:dynamic *sig-spy* "To support `with-signal`" nil) +(enc/defonce ^:dynamic *sig-handlers* "?[]" nil) + +(defn -with-signal + "Private util to support `with-signal` macro." + [form-fn + {:keys [return trap-errors? stop-propagation? force-msg?] + :or {return :vec}}] + + (let [sv_ (volatile! nil)] + (binding [*sig-spy* [sv_ stop-propagation?]] + (let [form-result + (if trap-errors? + (enc/try* {:okay (form-fn)} (catch :any t t {:error t})) + (do (form-fn))) + + signal + (when-let [sv @sv_] + (if-not force-msg? + sv + (if-let [e (find sv :msg_)] + (assoc sv :msg_ (force (val e))) + (do sv))))] + + (case return + :vec [form-result signal] + :form-result form-result + :signal signal + (enc/unexpected-arg! + {:context `with-signal + :param 'return + :expected #{:vec :form-result :signal}})))))) + +#?(:clj + (defmacro ^:public with-signal + "Util for tests/debugging. + Executes given form and returns [ ]. + If `trap-errors?` is true, form result will be wrapped by {:keys [okay error]}." + + {:arglists + '([form] + [{:keys [return trap-errors? stop-propagation? force-msg?]} + form])} + + ([ form] `(-with-signal (fn [] ~form) nil)) + ([opts form] `(-with-signal (fn [] ~form) ~opts)))) + +(defn dispatch-signal! + "Dispatches given signal to registered handlers, supports `with-signal`." + [signal] + (or + (when-let [[v stop-propagation?] *sig-spy*] + (vreset! v (sigs/signal-value signal nil)) + stop-propagation?) + + (sigs/call-handlers! *sig-handlers* signal))) + +;;;; Signal constructor + +(deftype RunResult [value error ^long runtime-nsecs] + #?(:clj clojure.lang.IFn :cljs IFn) + (#?(:clj invoke :cljs -invoke) [_] (if error (throw error) value))) + +(defn new-signal + "Returns a new `Signal` with given opts." + ^Signal + ;; Note all dynamic vals passed as explicit args for better control + [instant uid, + callsite-id location ns line column file, + sample-rate, kind id level, ctx parent, + user-opts data msg_, + run-form run-result error] + + (let [signal + (if-let [^RunResult run-result run-result] + (let [runtime-nsecs (.-runtime-nsecs run-result) + end-instant + #?(:clj (.plusNanos ^java.time.Instant instant runtime-nsecs) + :cljs (js/Date. (+ (.getTime instant) (/ runtime-nsecs 1e6)))) + + run-error (.-error run-result) + run-value (.-value run-result) + msg_ + (if (enc/identical-kw? msg_ ::spy) + (delay (str run-form " => " (or run-error run-value))) + msg_)] + + (Signal. 1 instant uid, + callsite-id location ns line column file, + sample-rate, kind id level, ctx parent, + data msg_, + run-error run-form run-value, + end-instant runtime-nsecs)) + + (Signal. 1 instant uid, + callsite-id location ns line column file, + sample-rate, kind id level, ctx parent, + data msg_, error nil nil instant nil))] + + (if user-opts + (reduce-kv assoc signal user-opts) + (do signal)))) + +(comment + (enc/qb 1e6 ; 55.67 + (new-signal + nil nil nil nil nil nil nil nil nil nil + nil nil nil nil nil nil nil nil nil nil))) + +;;;; Signal API helpers + +#?(:clj + (defn signal-arglists [macro-id] + (case macro-id + + :signal! ; [opts] => or + '([{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error run & user-opts]}]) + + :log! ; [msg] [level-or-opts msg] => + '([ msg] + [level msg] + [{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error #_run & user-opts]} + msg]) + + :event! ; [id] [level-or-opts id] => + '([ id] + [level id] + [{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error #_run & user-opts]} + id]) + + :error! ; [error] [id-or-opts error] => + '([ error] + [id error] + [{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error #_run & user-opts]} + error]) + + (:trace! :spy!) ; [form] [id-or-opts form] => (value or throw) + '([ form] + [id form] + [{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error run & user-opts]} + form]) + + :catch->error! ; [form] [level-or-opts form] => (value or throw) + '([ form] + [level form] + [{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, rethrow? catch-val, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error #_run & user-opts]} + form]) + + :uncaught->error! ; [] [id-or-opts] => nil + '([ ] + [id] + [{:as opts :keys + [#_defaults #_elide? #_allow? #_callsite-id, + elidable? location instant uid middleware, + sample-rate ns kind id level filter when rate-limit, + ctx parent trace?, let data msg error #_run & user-opts]}]) + + (enc/unexpected-arg! macro-id)))) + +#?(:clj + (defn signal-opts + "Util to help write common signal wrapper macros: + [[] val-y] => signal-opts + [[] opts-or-val-x val-y] => signal-opts" + ([arg-key or-key defaults arg-val] {:defaults defaults, arg-key arg-val}) + ([arg-key or-key defaults opts-or-key arg-val] + (if (map? opts-or-key) + (let [opts opts-or-key] (conj {:defaults defaults, arg-key arg-val} opts)) + (let [or-val opts-or-key] {:defaults defaults, arg-key arg-val, or-key or-val}))))) + +(comment + [(signal-opts :msg :level {:level :info} "foo") + (signal-opts :msg :level {:level :info} {:level :warn} "foo") + (signal-opts :msg :level {:level :info} :warn "foo")]) + +;;;; Signal macro + +#?(:clj + (defmacro ^:public signal! + "Expands to a low-level signal call. + + TODO Docstring + - How low-level is this? Should location, ctx, etc. be in public arglists? + - Describe + - Reference diagram link [1] + - Mention ability to delay-wrap :data + - Mention combo `:sample-rate` stuff (call * handler) + + - If :run => returns body run-result (re-throwing) + Otherwise returns true iff call allowed + + [1] Ref. " + {:arglists (signal-arglists :signal!)} + [opts] + (have? map? opts) ; We require const map keys, but vals may require eval + (let [defaults (get opts :defaults) + opts (merge defaults (dissoc opts :defaults)) + {run-form :run} opts + + {:keys [callsite-id location elide? allow?]} + (sigs/filterable-expansion + {:macro-form &form + :macro-env &env + :sf-arity 4 + :ct-sig-filter ct-sig-filter + :rt-sig-filter `*rt-sig-filter*} + opts)] + + (if elide? + run-form + (let [{:keys [ns line column file]} location + {instant-form :instant + kind-form :kind + id-form :id + level-form :level} opts + + trace? (get opts :trace? (boolean run-form)) + uid-form (get opts :uid (when trace? :auto/uuid-str)) + ctx-form (get opts :ctx `taoensso.telemere/*ctx*) + parent-form (get opts :parent (when trace? `taoensso.telemere.impl/*trace-parent*)) + instant-form (get opts :instant :auto) + instant-form (if (= instant-form :auto) `(enc/now-inst*) instant-form) + uid-form (parse-uid-form uid-form) + ;; run-fn-form (when run-form `(fn [] ~run-form)) + run-result-form + (when run-form + `(let [~'__t0 (enc/now-nano*)] + (with-tracing ~trace? ~'__id ~'__uid + (enc/try* + (do (RunResult. ~run-form nil (- (enc/now-nano*) ~'__t0))) + (catch :any ~'__t (RunResult. nil ~'__t (- (enc/now-nano*) ~'__t0))))))) + + signal-form + (let [{let-form :let + data-form :data + msg-form :msg + error-form :error + sample-rate-form :sample-rate} + opts + + let-form (or let-form '[]) + msg-form (parse-msg-form msg-form) + + ;; No, better leave it to user re: whether or not to delay-wrap + ;; data-form + ;; (when data-form + ;; (if (enc/call-in-form? data-form) + ;; `(delay ~data-form) + ;; (do data-form))) + + user-opts-form + (not-empty + (dissoc opts + :elidable? :location :instant :uid :middleware, + :sample-rate :ns :kind :id :level :filter :when #_:rate-limit, + :ctx :parent #_:trace?, :let :data :msg :error :run + :elide? :allow? :callsite-id))] + + ;; Eval let bindings AFTER call filtering but BEFORE data, msg + `(let ~let-form ; Allow to throw during `signal-value_` deref + (new-signal ~'__instant ~'__uid + ~callsite-id ~location ~ns ~line ~column ~file, + ~sample-rate-form, ~kind-form ~'__id ~level-form, ~ctx-form ~parent-form, + ~user-opts-form ~data-form ~msg-form, + '~run-form ~'__run-result ~error-form)))] + + #_ ; Sacrifice some perf to de-dupe (possibly large) `run-form` + (let [~'__run-fn ~run-fn-form] + (if-not ~allow? + (when ~'__run-fn (~'__run-fn)) + (let []))) + + `(enc/if-not ~allow? ; Allow to throw at call + ~run-form + (let [~'__instant ~instant-form ; Allow to throw at call + ~'__id ~id-form ; '' + ~'__uid ~uid-form ; '' + ~'__run-result ~run-result-form ; Non-throwing (traps) + + ~'__call-middleware + ~(get opts :middleware + `taoensso.telemere/*middleware*)] + + (dispatch-signal! + (WrappedSignal. ; Same internal value sent (conditionally) to all handlers + ~ns ~kind-form ~'__id ~level-form + + ;; Cache shared by all handlers. Covers signal `:let` eval, signal construction, + ;; middleware (possibly expensive), etc. + (delay + + ;; The unwrapped signal value actually visible to users/handler-fns, realized only + ;; AFTER handler filtering. Allowed to throw on deref (handler will catch). + (let [~'__signal ~signal-form] ; Can throw + (if ~'__call-middleware + ((sigs/get-middleware-fn ~'__call-middleware) ~'__signal) ; Can throw + (do ~'__signal)))))) + + (if ~'__run-result + (do (~'__run-result)) + true)))))))) + +(comment + (with-signal (signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)})) + (macroexpand '(signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)})) + + (do + (println "---") + (sigs/with-handler *sig-handlers* "hf1" (fn hf1 [x] (println x)) {} + (signal! {:level :info, :run "run"})))) + +#?(:clj + (defmacro signal-allowed? + "Used only for interop (SLF4J, `clojure.tools.logging`, etc.)." + {:arglists (signal-arglists :signal!)} + [opts] + (let [{:keys [#_callsite-id #_location elide? allow?]} + (sigs/filterable-expansion + {:macro-form &form + :macro-env &env + :sf-arity 4 + :ct-sig-filter ct-sig-filter + :rt-sig-filter `*rt-sig-filter*} + opts)] + + (and (not elide?) allow?)))) diff --git a/src/taoensso/telemere/tools_logging.clj b/src/taoensso/telemere/tools_logging.clj new file mode 100644 index 0000000..b0a2073 --- /dev/null +++ b/src/taoensso/telemere/tools_logging.clj @@ -0,0 +1,51 @@ +(ns ^:no-doc taoensso.telemere.tools-logging + "Private ns, implementation detail. + Interop support: `clojure.tools.logging` -> Telemere." + (:require + [clojure.tools.logging :as ctl] + [taoensso.encore :as enc :refer [have have?]] + [taoensso.telemere.impl :as impl])) + +(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body))) + +(deftype TelemereLogger [logger-ns] + + clojure.tools.logging.impl/Logger + (enabled? [_ level] + (when-debug (println [:tools.logger/enabled? logger-ns level])) + (impl/signal-allowed? + {:location nil + :ns nil + :kind :log + :id :taoensso.telemere/tools-logging + :level level})) + + (write! [_ level throwable message] + (when-debug (println [:tools.logger/write! logger-ns level])) + (impl/signal! + {:allow? true ; Pre-filtered by `enabled?` call + :location nil + :ns nil + :kind :log + :id :taoensso.telemere/tools-logging + :level level + :instant :auto + :error throwable + :msg message}) + nil)) + +(deftype TelemereLoggerFactory [] + clojure.tools.logging.impl/LoggerFactory + (name [_ ] "taoensso.telemere") + (get-logger [_ logger-ns] (TelemereLogger. (str logger-ns)))) + +(defn ^:public tools-logging->telemere! + "Configures `clojure.tools.logging` to use Telemere as its logging implementation." + [] + (alter-var-root #'clojure.tools.logging/*logger-factory* + (fn [_] (TelemereLoggerFactory.)))) + +(defn tools-logging-factory [] (TelemereLoggerFactory.)) +(defn tools-logging->telemere? [] + (when-let [lf clojure.tools.logging/*logger-factory*] + (instance? TelemereLoggerFactory lf))) diff --git a/test/taoensso/graal_tests.clj b/test/taoensso/graal_tests.clj new file mode 100644 index 0000000..8ed62bf --- /dev/null +++ b/test/taoensso/graal_tests.clj @@ -0,0 +1,5 @@ +(ns taoensso.graal-tests + (:require [taoensso.telemere :as telemere]) + (:gen-class)) + +(defn -main [& args] (println "Namespace loaded successfully")) diff --git a/test/taoensso/telemere_tests.cljc b/test/taoensso/telemere_tests.cljc new file mode 100644 index 0000000..3bc2a02 --- /dev/null +++ b/test/taoensso/telemere_tests.cljc @@ -0,0 +1,504 @@ +(ns taoensso.telemere-tests + (:require + [clojure.test :as test :refer [deftest testing is]] + [taoensso.encore :as enc :refer [throws? submap?]] + [taoensso.encore.signals :as sigs] + [taoensso.telemere :as tel] + [taoensso.telemere.impl :as impl] + #?(:clj [taoensso.telemere.slf4j :as slf4j]) + #?(:clj [clojure.tools.logging :as ctl])) + + #?(:cljs + (:require-macros + [taoensso.telemere-tests :refer [sig! ws ws*]]))) + +(comment + (remove-ns 'taoensso.telemere-tests) + (test/run-tests 'taoensso.telemere-tests)) + +;;;; Utils + +(enc/defaliases + #?(:clj {:alias sig! :src impl/signal!}) + #?(:default {:alias sm? :src enc/submap?})) + +#?(:clj (defmacro ws [form] `(impl/-with-signal (fn [] ~form) {}))) +#?(:clj (defmacro ws* [form] `(impl/-with-signal (fn [] ~form) {:trap-errors? true}))) +#?(:clj (defmacro wsv [form] `(impl/-with-signal (fn [] ~form) {:force-msg? true, :return :signal}))) + +(do + (def ^:private ex1 (ex-info "TestEx" {})) + (def ^:private ex1? #(= % ex1)) + (def ^:private ex1-rv? #(= (:error %) ex1)) + (def ^:private ex1-pred (enc/pred ex1?))) + +;;;; + +#?(:clj + (deftest _parse-msg-form + (let [pmf @#'impl/parse-msg-form] + [(is (= (pmf '["foo"])) '"foo") + (is (= (pmf '["foo" "bar"]) '(clojure.core/delay (taoensso.telemere.impl/signal-msg ["foo" "bar"])))) + (is (= (pmf 'my-symbol) 'my-symbol))]))) + +(deftest _impl-misc + ;; Note lots of low-level signal/filtering tests in `taoensso.encore` + [(is (= (impl/signal-msg + ["x" "y" nil ["z1" nil "z2" "z3"] + (impl/msg-splice ["s1" nil "s2" "s3" (impl/msg-skip) "s4"]) + (impl/msg-splice nil) + (impl/msg-skip) :kw]) + + "xynil[\"z1\" nil \"z2\" \"z3\"]s1nils2s3s4:kw"))]) + +(deftest _signal-macro + [(is (= (ws (sig! {:level :info, :elide? true })) [nil nil]) "With compile-time elision") + (is (= (ws (sig! {:level :info, :elide? true, :run (+ 1 2)})) [3 nil]) "With compile-time elision, run-form") + (is (= (ws (sig! {:level :info, :allow? false })) [nil nil]) "With runtime suppression") + (is (= (ws (sig! {:level :info, :allow? false, :run (+ 1 2)})) [3 nil]) "With runtime suppression, run-form") + + (is (->> (sig! {:level :info, :elide? true, :run (throw ex1)}) (throws? :ex-info "TestEx")) "With compile-time elision, throwing run-form") + (is (->> (sig! {:level :info, :allow? false, :run (throw ex1)}) (throws? :ex-info "TestEx")) "With runtime suppression, throwing run-form") + + (let [[rv1 sv1] (ws (sig! {:level :info })) + [rv2 sv2] (ws (sig! {:level :info, :run (+ 1 2)}))] + + [(is (= rv1 true)) (is (sm? sv1 {:ns "taoensso.telemere-tests", :level :info, :run-form nil, :run-value nil, :runtime-nsecs nil})) + (is (= rv2 3)) (is (sm? sv2 {:ns "taoensso.telemere-tests", :level :info, :run-form '(+ 1 2), :run-value 3, :runtime-nsecs (enc/pred nat-int?)}))]) + + (testing "Nested signals" + (let [[[inner-rv inner-sv] outer-sv] (ws (sig! {:level :info, :run (ws (sig! {:level :warn, :run "inner-run"}))}))] + [(is (= inner-rv "inner-run")) + (is (sm? inner-sv {:level :warn, :run-value "inner-run"})) + (is (sm? outer-sv {:level :info :run-value [inner-rv inner-sv]}))])) + + (testing "Instants" + (let [[_ sv1] (ws (sig! {:level :info })) + [_ sv2] (ws (sig! {:level :info, :run (reduce + (range 1e6))})) + [_ sv3] (ws (sig! {:level :info, :run (reduce + (range 1e6)) + :instant ; Allow custom instant + #?(:clj java.time.Instant/EPOCH + :cljs (js/Date. 0))}))] + + [(let [{start :instant, end :end-instant} sv1] + [(is (enc/inst? start)) + (is (enc/inst? end)) + (is (= start end))]) + + (let [{start :instant, end :end-instant} sv2] + [(is (enc/inst? start)) + (is (enc/inst? end)) + (is (> (inst-ms end) (inst-ms start)))]) + + (let [{start :instant, end :end-instant} sv3] + [(is (enc/inst? start)) + (is (enc/inst? end)) + (is (= (inst-ms start) 0) "Respect custom instant") + (is (> (inst-ms end) (inst-ms start)) "End instant is start + runtime-nsecs") + (is (< (inst-ms end) 1e6) "End instant is start + runtime-nsecs")])])) + + (testing "User opts assoced directly to signal" + (let [[rv sv] (ws (sig! {:level :info, :my-opt1 "v1", :my-opt2 "v2"}))] + (is (sm? sv {:level :info, :my-opt1 "v1", :my-opt2 "v2"})))) + + (testing "`:msg` basics" + (let [c (enc/counter) + [rv1 sv1] (ws (sig! {:level :info, :run (c), :msg "msg1"})) ; No delay + [rv2 sv2] (ws (sig! {:level :info, :run (c), :msg [ "msg2:" (c)]})) ; Auto delay + [rv3 sv3] (ws (sig! {:level :info, :run (c), :msg (delay (str "msg3:" (c)))})) ; Manual delay + [rv4 sv4] (ws (sig! {:level :info, :run (c), :msg (str "msg4:" (c))})) ; No delay + [rv5 sv5] (ws (sig! {:level :info, :run (c), :msg (str "msg5:" (c)), :allow? false}))] + + [(is (= rv1 0)) (is (= (:msg_ sv1) "msg1")) + (is (= rv2 1)) (is (= @(:msg_ sv2) "msg2:6")) + (is (= rv3 2)) (is (= @(:msg_ sv3) "msg3:7")) + (is (= rv4 3)) (is (= (:msg_ sv4) "msg4:4")) + (is (= rv5 5)) (is (= (:msg_ sv5) nil)) + (is (= @c 8) "5x run + 3x message (1x suppressed)")])) + + (testing "`:data` basics" + (vec + (for [dk [:data :my-opt]] ; User opts share same behaviour as data + (let [c (enc/counter) + [rv1 sv1] (ws (sig! {:level :info, :run (c), dk {:c1 (c)}})) + [rv2 sv2] (ws (sig! {:level :info, :run (c), dk (delay {:c2 (c)})})) + [rv3 sv3] (ws (sig! {:level :info, :run (c), dk {:c3 (c)}, :allow? false})) + [rv4 sv4] (ws (sig! {:level :info, :run (c), dk (delay {:c4 (c)}), :allow? false})) + [rv5 sv5] (ws (sig! {:level :info, :run (c), dk [:c5 (c)]})) + [rv6 sv6] (ws (sig! {:level :info, :run (c), dk (delay [:c6 (c)])}))] + + [(is (= rv1 0)) (is (= (get sv1 dk) {:c1 1})) + (is (= rv2 2)) (is (= (force (get sv2 dk)) {:c2 8})) + (is (= rv3 3)) (is (= (get sv3 dk) nil)) + (is (= rv4 4)) (is (= (force (get sv4 dk)) nil)) + (is (= rv5 5)) (is (= (get sv5 dk) [:c5 6]) "`:data` can be any type") + (is (= rv6 7)) (is (= (force (get sv6 dk)) [:c6 9]) "`:data` can be any type") + (is (= @c 10) "6x run + 4x data (2x suppressed)")])))) + + (testing "`:let` basics" + (let [c (enc/counter) + [rv1 sv1] (ws (sig! {:level :info, :run (c), :let [_ (c)]})) + [rv2 sv2] (ws (sig! {:level :info, :run (c), :let [_ (c)], :allow? false})) + [rv3 sv3] (ws (sig! {:level :info, :run (c), :let [_ (c)]}))] + + [(is (= rv1 0)) + (is (= rv2 2)) + (is (= rv3 3)) + (is (= @c 5) "3x run + 2x let (1x suppressed)")])) + + (testing "`:let` + `:msg`" + (let [c (enc/counter) + [rv1 sv1] (ws (sig! {:level :info, :run (c), :let [n (c)], :msg "msg1"})) ; No delay + [rv2 sv2] (ws (sig! {:level :info, :run (c), :let [n (c)], :msg [ "msg2:" n ":" (c)]})) ; Auto delay + [rv3 sv3] (ws (sig! {:level :info, :run (c), :let [n (c)], :msg (delay (str "msg3:" n ":" (c)))})) ; Manual delay + [rv4 sv4] (ws (sig! {:level :info, :run (c), :let [n (c)], :msg (str "msg4:" n ":" (c))})) ; No delay + [rv5 sv5] (ws (sig! {:level :info, :run (c), :let [n (c)], :msg (str "msg5:" n ":" (c)), :allow? false}))] + + [(is (= rv1 0)) (is (= (:msg_ sv1) "msg1")) + (is (= rv2 2)) (is (= @(:msg_ sv2) "msg2:3:10")) + (is (= rv3 4)) (is (= @(:msg_ sv3) "msg3:5:11")) + (is (= rv4 6)) (is (= (:msg_ sv4) "msg4:7:8")) + (is (= rv5 9)) (is (= (:msg_ sv5) nil)) + (is (= @c 12) "5x run + 4x let (1x suppressed) + 3x msg (1x suppressed)")])) + + (testing "`:let` + `:data`" + (vec + (for [dk [:data :my-opt]] + (let [c (enc/counter) + [rv1 sv1] (ws (sig! {:level :info, :run (c), :let [n (c)], dk {:n n, :c1 (c)}})) + [rv2 sv2] (ws (sig! {:level :info, :run (c), :let [n (c)], dk (delay {:n n, :c2 (c)})})) + [rv3 sv3] (ws (sig! {:level :info, :run (c), :let [n (c)], dk {:n n, :c3 (c)}, :allow? false})) + [rv4 sv4] (ws (sig! {:level :info, :run (c), :let [n (c)], dk (delay {:n n, :c4 (c)}), :allow? false})) + [rv5 sv5] (ws (sig! {:level :info, :run (c), :let [n (c)], dk [:n n, :c5 (c)]})) + [rv6 sv6] (ws (sig! {:level :info, :run (c), :let [n (c)], dk (delay [:n n, :c6 (c)])}))] + + [(is (= rv1 0)) (is (= (get sv1 dk) {:n 1, :c1 2})) + (is (= rv2 3)) (is (= (force (get sv2 dk)) {:n 4, :c2 12})) + (is (= rv3 5)) (is (= (get sv3 dk) nil)) + (is (= rv4 6)) (is (= (force (get sv4 dk)) nil)) + (is (= rv5 7)) (is (= (get sv5 dk) [:n 8, :c5 9])) + (is (= rv6 10)) (is (= (force (get sv6 dk)) [:n 11, :c6 13])) + (is (= @c 14) "6x run + 4x let (2x suppressed) + 4x data (2x suppressed)")])))) + + (testing "Manual `let` (unconditional) + `:data`" + (vec + (for [dk [:data #_:my-opt]] + (let [c (enc/counter) + [rv1 sv1] (ws (let [n (c)] (sig! {:level :info, :run (c), dk {:n n, :c1 (c)}}))) + [rv2 sv2] (ws (let [n (c)] (sig! {:level :info, :run (c), dk (delay {:n n, :c2 (c)})}))) + [rv3 sv3] (ws (let [n (c)] (sig! {:level :info, :run (c), dk {:n n, :c3 (c)}, :allow? false}))) + [rv4 sv4] (ws (let [n (c)] (sig! {:level :info, :run (c), dk (delay {:n n, :c4 (c)}), :allow? false}))) + [rv5 sv5] (ws (let [n (c)] (sig! {:level :info, :run (c), dk [:n n, :c5 (c)]}))) + [rv6 sv6] (ws (let [n (c)] (sig! {:level :info, :run (c), dk (delay [:n n, :c6 (c)])})))] + + [ + (is (= rv1 1)) (is (= (get sv1 dk) {:n 0, :c1 2})) + (is (= rv2 4)) (is (= (force (get sv2 dk)) {:n 3, :c2 14})) + (is (= rv3 6)) (is (= (get sv3 dk) nil)) + (is (= rv4 8)) (is (= (force (get sv4 dk)) nil)) + (is (= rv5 10)) (is (= (get sv5 dk) [:n 9, :c5 11])) + (is (= rv6 13)) (is (= (force (get sv6 dk)) [:n 12, :c6 15])) + (is (= @c 16) "6x run + 6x let (0x suppressed) + 4x data (2x suppressed)")])))) + + (testing "Call middleware" + (let [c (enc/counter) + [rv1 sv1] (ws (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})) + [rv2 sv2] (ws (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false})) + [rv3 sv3] (ws (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})) + [rv4 sv4] (ws (sig! {:level :info, :middleware [(fn [_] "signal-value")]}))] + + [(is (= rv1 0)) (is (sm? sv1 {:m1 1 :m2 2})) + (is (= rv2 3)) (is (nil? sv2)) + (is (= rv3 4)) (is (sm? sv3 {:m1 5 :m2 6})) + (is (= rv4 true)) (is (= sv4 "signal-value")) + (is (= @c 7) "3x run + 4x middleware")]))]) + +(deftest _handlers + ;; Basic handler tests are in Encore + [(testing "Handler middleware" + (let [c (enc/counter) + sv-h1_ (atom nil) + sv-h2_ (atom nil) + wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]}) + wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})] + + ;; Note that call middleware output is cached and shared across all handlers + (binding [impl/*sig-handlers* [wh1 wh2]] + (let [;; 1x run + 4x handler middleware + 2x call middleware = 7x + rv1 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}) + sv1-h1 @sv-h1_ + sv1-h2 @sv-h2_ + c1 @c + + ;; 1x run + rv2 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false}) + sv2-h1 @sv-h1_ + sv2-h2 @sv-h2_ + c2 @c ; 8 + + ;; 1x run + 4x handler middleware + 2x call middleware = 7x + rv3 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}) + sv3-h1 @sv-h1_ + sv3-h2 @sv-h2_ + c3 @c ; 15 + + ;; 4x handler middleware + rv4 (sig! {:level :info, :middleware [(fn [_] {:my-sig-val? true})]}) + sv4-h1 @sv-h1_ + sv4-h2 @sv-h2_ + c4 @c] + + [(is (= rv1 0)) (is (sm? sv1-h1 {:m1 1, :m2 2, :hm1 3, :hm2 4})) (is (sm? sv1-h2 {:m1 1, :m2 2, :hm1 5, :hm2 6})) + (is (= rv2 7)) (is (sm? sv2-h1 {:m1 1, :m2 2, :hm1 3, :hm2 4})) (is (sm? sv2-h2 {:m1 1, :m2 2, :hm1 5, :hm2 6})) + (is (= rv3 8)) (is (sm? sv3-h1 {:m1 9, :m2 10, :hm1 11, :hm2 12})) (is (sm? sv3-h2 {:m1 9, :m2 10, :hm1 13, :hm2 14})) + (is (= rv4 true)) (is (sm? sv4-h1 {:my-sig-val? true, :hm1 15, :hm2 16})) (is (sm? sv4-h2 {:my-sig-val? true, :hm1 17, :hm2 18})) + (is (= c1 7) "1x run + 4x handler middleware + 2x call middleware") + (is (= c2 8) "2x run + 4x handler middleware + 2x call middleware") + (is (= c3 15) "3x run + 8x handler middleware + 4x call middleware") + (is (= c4 19) "3x run + 12x handler middleware + 4x call middleware")]))))]) + +(def ^:private ^:dynamic *throwing-handler-middleware?* false) + +(deftest _throwing + (let [sv_ (atom :nx) + error_ (atom :nx) + reset-state! + (fn [] + (reset! sv_ :nx) + (reset! error_ :nx) + true)] + + (tel/with-handler :hid1 + (fn [sv] (force (:data sv)) (reset! sv_ sv)) + {:async nil, :error-fn (fn [x] (reset! error_ x)), :rl-error nil, + :middleware [(fn [sv] (if *throwing-handler-middleware?* (throw ex1) sv))]} + + [(is (->> (sig! {:level :info, :filter (throw ex1)}) (throws? :ex-info "TestEx")) "`~filterable-expansion/allow` throws at call") + (is (->> (sig! {:level :info, :instant (throw ex1)}) (throws? :ex-info "TestEx")) "`~instant-form` throws at call") + (is (->> (sig! {:level :info, :id (throw ex1)}) (throws? :ex-info "TestEx")) "`~id-form` throws at call") + (is (->> (sig! {:level :info, :uid (throw ex1)}) (throws? :ex-info "TestEx")) "`~uid-form` throws at call") + (is (->> (sig! {:level :info, :run (throw ex1)}) (throws? :ex-info "TestEx")) "`~run-form` rethrows at call") + (is (sm? @sv_ {:level :info, :error ex1-pred}) "`~run-form` rethrows at call *after* dispatch") + + (testing "`@signal-value_`: trap with wrapped handler" + [(testing "Throwing `~let-form`" + (reset-state!) + [(is (true? (sig! {:level :info, :let [_ (throw ex1)]}))) + (is (= @sv_ :nx)) + (is (sm? @error_ {:handler-id :hid1, :error ex1-pred}))]) + + (testing "Throwing call middleware" + (reset-state!) + [(is (true? (sig! {:level :info, :middleware [(fn [_] (throw ex1))]}))) + (is (= @sv_ :nx)) + (is (sm? @error_ {:handler-id :hid1, :error ex1-pred}))]) + + (testing "Throwing handler middleware" + (reset-state!) + (binding [*throwing-handler-middleware?* true] + [(is (true? (sig! {:level :info}))) + (is (= @sv_ :nx)) + (is (sm? @error_ {:handler-id :hid1, :error ex1-pred}))])) + + (testing "Throwing `@data_`" + (reset-state!) + [(is (true? (sig! {:level :info, :data (delay (throw ex1))}))) + (is (= @sv_ :nx)) + (is (sm? @error_ {:handler-id :hid1, :error ex1-pred}))]) + + (testing "Throwing user opt" + (reset-state!) + [(is (true? (sig! {:level :info, :my-opt (throw ex1)}))) + (is (= @sv_ :nx)) + (is (sm? @error_ {:handler-id :hid1, :error ex1-pred}))])])]))) + +(deftest _ctx + (testing "Context (`*ctx*`)" + [(is (= (binding [tel/*ctx* "my-ctx"] tel/*ctx*) "my-ctx") "Supports manual `binding`") + (is (= (tel/with-ctx "my-ctx" tel/*ctx*) "my-ctx") "Supports any data type") + + (is (= (tel/with-ctx "my-ctx1" (tel/with-ctx+ nil tel/*ctx*)) "my-ctx1") "nil update => keep old-ctx") + (is (= (tel/with-ctx "my-ctx1" (tel/with-ctx+ (fn [old] [old "my-ctx2"]) tel/*ctx*)) ["my-ctx1" "my-ctx2"]) "fn update => apply") + (is (= (tel/with-ctx {:a :A1 :b :B1} (tel/with-ctx+ {:a :A2 :c :C2} tel/*ctx*)) {:a :A2 :b :B1 :c :C2}) "map update => merge") + + (let [[_ sig] (ws (sig! {:level :info, :ctx "my-ctx"}))] (is (sm? sig {:ctx "my-ctx"}) "Can be set via call opt"))])) + +(deftest _tracing + (testing "Tracing" + [(let [[_ sv] (ws (sig! {:level :info }))] (is (sm? sv {:parent nil}))) + (let [[_ sv] (ws (sig! {:level :info, :parent {:id :id0}}))] (is (sm? sv {:parent {:id :id0 :uid :submap/nx}}) "`:parent/id` can be set via call opt")) + (let [[_ sv] (ws (sig! {:level :info, :parent {:uid :uid0}}))] (is (sm? sv {:parent {:id :submap/nx :uid :uid0}}) "`:parent/uid` can be set via call opt")) + + (testing "Auto call id, uid" + (let [[_ sv] (ws (sig! {:level :info, :parent {:id :id0, :uid :uid0}, :run impl/*trace-parent*, :data impl/*trace-parent*}))] + [(is (sm? sv {:parent {:id :id0, :uid :uid0}})) + (is (sm? sv {:run-value {:id nil, :uid (get sv :uid ::nx)}}) "`*trace-parent*` visible to run-form, bound to call's auto {:keys [id uid]}") + (is (sm? sv {:data nil}) "`*trace-parent*` not visible to data-form ")])) + + (testing "Manual call id, uid" + (let [[_ sv] (ws (sig! {:level :info, :parent {:id :id0, :uid :uid0}, :id :id1, :uid :uid1, :run impl/*trace-parent*, :data impl/*trace-parent*}))] + [(is (sm? sv {:parent {:id :id0, :uid :uid0}})) + (is (sm? sv {:run-value {:id :id1, :uid :uid1}}) "`*trace-parent*` visible to run-form, bound to call's auto {:keys [id uid]}") + (is (sm? sv {:data nil}) "`*trace-parent*` not visible to data-form ")])) + + (testing "Tracing can be disabled via call opt" + (let [[_ sv] (ws (sig! {:level :info, :parent {:id :id0, :uid :uid0}, :id :id1, :uid :uid1, :run impl/*trace-parent*, :data impl/*trace-parent*, :trace? false}))] + [(is (sm? sv {:parent {:id :id0, :uid :uid0}})) + (is (sm? sv {:run-value nil}))])) + + (testing "Signal nesting" + (let [[[inner-rv inner-sv] outer-sv] + (ws (sig! { :level :info, :id :id1, :uid :uid1, + :run (ws (sig! {:level :info, :id :id2, :uid :uid2, :run impl/*trace-parent*}))}))] + + [(is (sm? outer-sv {:id :id1, :uid :uid1, :parent nil})) + (is (sm? inner-rv {:id :id2, :uid :uid2})) + (is (sm? inner-sv {:parent {:id :id1, :uid :uid1}})) + (is (sm? inner-sv {:run-value {:id :id2, :uid :uid2}}))]))])) + +(deftest _sampling + ;; Capture combined (call * handler) sample rate in Signal when possible + (let [test1 + (fn [call-sample-rate handler-sample-rate] + (let [c (enc/counter) + sr_ (atom nil)] + (tel/with-handler "h1" + (fn h1 [x] (c) (compare-and-set! sr_ nil (:sample-rate x))) + {:async nil, :sample-rate handler-sample-rate} + (do + ;; Repeat to ensure >=1 gets through sampling + (dotimes [_ 1000] (sig! {:level :info, :sample-rate call-sample-rate})) + [@sr_ @c]))))] + + [(is (= (test1 nil nil) [nil 1000]) "[none none] = none") + (is (= (test1 nil (fn [] nil)) [nil 1000]) "[none =>none] = none") + (is (= (test1 1.0 nil) [1.0 1000]) "[100% none] = 100%") + (is (= (test1 1.0 (fn [] nil)) [1.0 1000]) "[100% none] = 100%") + (is (= (test1 nil 1.0) [1.0 1000]) "[none 100%] = 100%") + (is (= (test1 nil (fn [] 1.0)) [1.0 1000]) "[none =>100%] = 100%") + + (is (= (test1 0.0 nil) [nil 0]) "[0% none] = 0%") + (is (= (test1 0.0 (fn [] nil)) [nil 0]) "[0% =>none] = 0%") + (is (= (test1 nil 0.0) [nil 0]) "[none 0%] = 0%") + (is (= (test1 nil (fn [] 0.0)) [nil 0]) "[none =>0%] = 0%") + + (let [[sr n] (test1 0.5 0.5) ] (is (and (= sr 0.25) (<= 150 n 350)) "[50% 50%] = 25%")) + (let [[sr n] (test1 0.5 (fn [] 0.5))] (is (and (= sr 0.25) (<= 150 n 350)) "[50% =>50%] = 25%"))])) + +;;;; + +(deftest _common-signals + [#?(:clj + (testing "signal-opts" + [(is (= (impl/signal-opts :msg, :level, {:level :info} "msg") {:defaults {:level :info}, :msg "msg"})) + (is (= (impl/signal-opts :msg, :level, {:level :info} {:level :warn} "msg") {:defaults {:level :info}, :msg "msg", :level :warn})) + (is (= (impl/signal-opts :msg, :level, {:level :info} :warn "msg") {:defaults {:level :info}, :msg "msg", :level :warn}))])) + + (testing "log!" ; msg + ?level => allowed? + [(let [[rv sv] (ws (tel/log! "msg"))] [(is (= rv true)) (is (sm? sv {:kind :log, :line :submap/ex, :msg_ "msg", :level :info}))]) + (let [[rv sv] (ws (tel/log! :warn "msg"))] [(is (= rv true)) (is (sm? sv {:kind :log, :line :submap/ex, :msg_ "msg", :level :warn}))]) + (let [[rv sv] (ws (tel/log! {:level :warn} "msg"))] [(is (= rv true)) (is (sm? sv {:kind :log, :line :submap/ex, :msg_ "msg", :level :warn}))]) + (let [[rv sv] (ws (tel/log! {:allow? false} "msg"))] [(is (= rv nil)) (is (nil? sv))])]) + + (testing "event!" ; id + ?level => allowed? + [(let [[rv sv] (ws (tel/event! :id1))] [(is (= rv true)) (is (sm? sv {:kind :event, :line :submap/ex, :level :info, :id :id1}))]) + (let [[rv sv] (ws (tel/event! :warn :id1))] [(is (= rv true)) (is (sm? sv {:kind :event, :line :submap/ex, :level :warn, :id :id1}))]) + (let [[rv sv] (ws (tel/event! {:level :warn} :id1))] [(is (= rv true)) (is (sm? sv {:kind :event, :line :submap/ex, :level :warn, :id :id1}))]) + (let [[rv sv] (ws (tel/event! {:allow? false} :id1))] [(is (= rv nil)) (is (nil? sv))])]) + + (testing "error!" ; error + ?id => error + [(let [[rv sv] (ws (tel/error! ex1))] [(is (= rv ex1)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id nil}))]) + (let [[rv sv] (ws (tel/error! :id1 ex1))] [(is (= rv ex1)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id :id1}))]) + (let [[rv sv] (ws (tel/error! {:id :id1} ex1))] [(is (= rv ex1)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id :id1}))]) + (let [[rv sv] (ws (tel/error! {:allow? false} ex1))] [(is (= rv ex1)) (is (nil? sv))])]) + + (testing "trace!" ; run + ?id => run result (value or throw) + [(let [[rv sv] (ws (tel/trace! (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id nil}))]) + (let [[rv sv] (ws (tel/trace! :id1 (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id :id1}))]) + (let [[rv sv] (ws (tel/trace! {:id :id1} (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id :id1}))]) + (let [[rv sv] (ws* (tel/trace! :id1 (throw ex1)))] [(is (ex1-rv? rv)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id :id1, :error ex1-pred}))]) + (let [[rv sv] (ws (tel/trace! {:allow? false} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])]) + + (testing "spy" ; run + ?level => run result (value or throw) + [(let [[rv sv] (ws (tel/spy! (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :info}))]) + (let [[rv sv] (ws (tel/spy! :warn (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :warn}))]) + (let [[rv sv] (ws (tel/spy! {:level :warn} (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :warn}))]) + (let [[rv sv] (ws* (tel/spy! :warn (throw ex1)))] [(is (ex1-rv? rv)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :warn, :error ex1-pred}))]) + (let [[rv sv] (ws (tel/spy! {:allow? false} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])]) + + (testing "catch->error!" ; form + ?id => run value or ?return + [(let [[rv sv] (ws (tel/catch->error! (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))]) + (let [[rv sv] (ws (tel/catch->error! (throw ex1)))] [(is (= rv nil)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id nil}))]) + (let [[rv sv] (ws (tel/catch->error! :id1 (throw ex1)))] [(is (= rv nil)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id :id1}))]) + (let [[rv sv] (ws (tel/catch->error! {:id :id1} (throw ex1)))] [(is (= rv nil)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id :id1}))]) + (let [[rv sv] (ws* (tel/catch->error! {:rethrow? true} (throw ex1)))] [(is (ex1-rv? rv)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id nil}))]) + (let [[rv sv] (ws (tel/catch->error! {:catch-val :foo} (throw ex1)))] [(is (= rv :foo)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id nil}))]) + (let [[rv sv] (ws (tel/catch->error! {:catch-val :foo} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))]) + (let [[rv sv] (ws (tel/catch->error! {:catch-val :foo ; Overrides `:rethrow?` + :rethrow? true} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])]) + + #?(:clj + (testing "uncaught->error!" + (let [sv_ (atom ::nx)] + [(do (enc/set-var-root! impl/*sig-handlers* [(sigs/wrap-handler "h1" (fn h1 [x] (reset! sv_ x)) nil {:async nil})]) :set-handler) + ;; + (is (nil? (tel/uncaught->error!))) + (is (do (.join (impl/threaded (throw ex1))) (sm? @sv_ {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id nil}))) + ;; + (is (nil? (tel/uncaught->error! :id1))) + (is (do (.join (impl/threaded (throw ex1))) (sm? @sv_ {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id :id1}))) + ;; + (is (nil? (tel/uncaught->error! {:id :id1}))) + (is (do (.join (impl/threaded (throw ex1))) (sm? @sv_ {:kind :error, :line :submap/ex, :level :error, :error ex1-pred, :id :id1}))) + ;; + (do (enc/set-var-root! impl/*sig-handlers* nil) :unset-handler)])))]) + +;;;; Interop + +(comment (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MyTelemereSLF4JLogger"))) + +#?(:clj + (deftest _interop + [(testing "`clojure.tools.logging` -> Telemere" + [(is (sm? (tel/interop-check) {:tools-logging {:present? true, :send->telemere? true, :receiving? true}})) + (is (sm? (wsv (ctl/info "Hello" "x" "y")) {:level :info, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/tools-logging, :msg_ "Hello x y"})) + (is (sm? (wsv (ctl/warn "Hello" "x" "y")) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/tools-logging, :msg_ "Hello x y"})) + (is (sm? (wsv (ctl/error ex1 "An error")) {:level :error, :error ex1}) "Errors")]) + + (testing "SLF4J -> Telemere" + [(is (sm? (tel/interop-check) {:slf4j {:present? true, :send->telemere? true, :receiving? true}})) + (let [^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MyTelemereSLF4JLogger")] + [(testing "Basics" + [(is (sm? (wsv (.info sl "Hello")) {:level :info, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello"}) "Legacy API: info basics") + (is (sm? (wsv (.warn sl "Hello")) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello"}) "Legacy API: warn basics") + (is (sm? (wsv (-> (.atInfo sl) (.log "Hello"))) {:level :info, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello"}) "Fluent API: info basics") + (is (sm? (wsv (-> (.atWarn sl) (.log "Hello"))) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello"}) "Fluent API: warn basics")]) + + (testing "Message formatting" + (let [msgp "X is {} and Y is {}", expected {:msg_ "X is x and Y is y", :data {:slf4j/args ["x" "y"]}}] + [(is (sm? (wsv (.info sl msgp "x" "y")) expected) "Legacy API: formatted message, raw args") + (is (sm? (wsv (-> (.atInfo sl) (.setMessage msgp) (.addArgument "x") (.addArgument "y") (.log))) expected) "Fluent API: formatted message, raw args")])) + + (is (sm? (wsv (-> (.atInfo sl) (.addKeyValue "k1" "v1") (.addKeyValue "k2" "v2") (.log))) {:data {:slf4j/kvs {"k1" "v1", "k2" "v2"}}}) "Fluent API: kvs") + + (testing "Markers" + (let [m1 (slf4j/est-marker! "M1") + m2 (slf4j/est-marker! "M2") + cm (slf4j/est-marker! "Compound" "M1" "M2")] + + [(is (sm? (wsv (.info sl cm "Hello")) {:data #:slf4j{:marker-names #{"Compound" "M1" "M2"}}}) "Legacy API: markers") + (is (sm? (wsv (-> (.atInfo sl) (.addMarker m1) (.addMarker cm) (.log))) {:data #:slf4j{:marker-names #{"Compound" "M1" "M2"}}}) "Fluent API: markers")])) + + (testing "Errors" + [(is (sm? (wsv (.warn sl "An error" ^Throwable ex1)) {:level :warn, :error ex1}) "Legacy API: errors") + (is (sm? (wsv (-> (.atWarn sl) (.setCause ex1) (.log))) {:level :warn, :error ex1}) "Fluent API: errors")]) + + (testing "MDC (Mapped Diagnostic Context)" + (with-open [_ (org.slf4j.MDC/putCloseable "k1" "v1")] + (with-open [_ (org.slf4j.MDC/putCloseable "k2" "v2")] + [(is (sm? (wsv (-> sl (.info "Hello"))) {:level :info, :ctx {"k1" "v1", "k2" "v2"}}) "Legacy API: MDC") + (is (sm? (wsv (-> (.atInfo sl) (.log "Hello"))) {:level :info, :ctx {"k1" "v1", "k2" "v2"}}) "Fluent API: MDC")])))])])])) + +;;;; + +#?(:cljs (test/run-tests)) diff --git a/wiki/.gitignore b/wiki/.gitignore new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/wiki/.gitignore @@ -0,0 +1 @@ +README.md diff --git a/wiki/1-Getting-started.md b/wiki/1-Getting-started.md new file mode 100644 index 0000000..52df1d4 --- /dev/null +++ b/wiki/1-Getting-started.md @@ -0,0 +1,14 @@ +# Setup + +Add the [relevant dependency](../#latest-releases) to your project: + +```clojure +Leiningen: [com.taoensso/telemere "x-y-z"] ; or +deps.edn: com.taoensso/telemere {:mvn/version "x-y-z"} +``` + +And setup your namespace imports: + +```clojure +(ns my-app (:require [taoensso.telemere :as tm])) +``` diff --git a/wiki/Home.md b/wiki/Home.md index 0df0bb6..af3d591 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1 +1,8 @@ -Init \ No newline at end of file +See the menu to the right for content 👉 + +# Contributions welcome + +**PRs very welcome** to help improve this documentation! +See the [wiki](../tree/master/wiki) folder in the main repo for the relevant files. + +\- [Peter Taoussanis](https://www.taoensso.com) \ No newline at end of file diff --git a/wiki/README.md b/wiki/README.md new file mode 100644 index 0000000..03088c3 --- /dev/null +++ b/wiki/README.md @@ -0,0 +1,5 @@ +# Attention! + +This wiki is designed for viewing from [here](../../../wiki)! + +Viewing from GitHub's file browser will result in **broken links**.