[new] Add initial code, content

This commit is contained in:
Peter Taoussanis 2023-10-16 10:50:36 +02:00
parent 16600b9c78
commit dca61ba582
26 changed files with 2233 additions and 3 deletions

32
.github/workflows/graal-tests.yml vendored Normal file
View file

@ -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

30
.github/workflows/main-tests.yml vendored Normal file
View file

@ -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

3
CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning).
---

2
FUNDING.yml Normal file
View file

@ -0,0 +1,2 @@
github: ptaoussanis
custom: "https://www.taoensso.com/clojure"

View file

@ -1,2 +1,57 @@
# telemere
Coming later
<a href="https://www.taoensso.com/clojure" title="More stuff by @ptaoussanis at www.taoensso.com"><img src="https://www.taoensso.com/open-source.png" alt="Taoensso open source" width="340"/></a>
[**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 &copy; 2023-2024 [Peter Taoussanis][].
Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure).
<!-- Common -->
[GitHub releases]: ../../releases
[GitHub issues]: ../../issues
[GitHub wiki]: ../../wiki
[Peter Taoussanis]: https://www.taoensso.com
[sponsor]: https://www.taoensso.com/sponsor
<!-- Project -->
[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

13
SECURITY.md Normal file
View file

@ -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)

10
bb.edn Normal file
View file

@ -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))}}}

38
bb/graal_tests.clj Executable file
View file

@ -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!")))

2
doc/cljdoc.edn Normal file
View file

@ -0,0 +1,2 @@
{:cljdoc/docstring-format :plaintext}

86
project.clj Normal file
View file

@ -0,0 +1,86 @@
(defproject com.taoensso/telemere "1.0.0-SNAPSHOT"
:author "Peter Taoussanis <https://www.taoensso.com>"
: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"]]})

16
slf4j/.gitignore vendored Normal file
View file

@ -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

22
slf4j/project.clj Normal file
View file

@ -0,0 +1,22 @@
(defproject com.taoensso/slf4j-telemere "1.0.0-SNAPSHOT"
:author "Peter Taoussanis <https://www.taoensso.com>"
: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"]]})

View file

@ -0,0 +1 @@
com.taoensso.telemere.slf4j.TelemereServiceProvider

View file

@ -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
}
}

View file

@ -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<String, Logger> 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();
}
}

View file

@ -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();
}
}

View file

@ -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. <https://www.slf4j.org/faq.html#slf4j_compatible>:
- 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 #{<MarkerName>}. 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)))

431
src/taoensso/telemere.cljc Normal file
View file

@ -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:
<https://www.taoensso.com/telemere>"
{: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 <js/Error>) better than string stacktrace (clickable, etc.)
;;
;; - Tests for utils (hostname, formatters, etc.)?
;; - Remaining docstrings and TODOs
;; - Document kinds: #{:log :spy :trace :event :error <user>}
;; - 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 {<scope-id> <data>} 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! <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 [`<java.lang.Thread>` `<java.lang.Throwable>`]).
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"}))]))

View file

@ -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* "?[<wrapped-handler-fn>]" 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 [<form-result> <last-signal-dispatched-by-form>].
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] => <run result> or <allowed?>
'([{: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] => <allowed?>
'([ 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] => <allowed?>
'([ 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>
'([ 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] => <run result> (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] => <run result> (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:
[[<config>] val-y] => signal-opts
[[<config>] 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. <https://github.com/taoensso/telemere/blob/master/signal-flow.svg>"
{: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?))))

View file

@ -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)))

View file

@ -0,0 +1,5 @@
(ns taoensso.graal-tests
(:require [taoensso.telemere :as telemere])
(:gen-class))
(defn -main [& args] (println "Namespace loaded successfully"))

View file

@ -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))

1
wiki/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
README.md

14
wiki/1-Getting-started.md Normal file
View file

@ -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]))
```

View file

@ -1 +1,8 @@
Init
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)

5
wiki/README.md Normal file
View file

@ -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**.