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
+
+[**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**.