├── wiki
├── .gitignore
├── README.md
├── Home.md
├── 2-Architecture.md
├── 8-Community.md
├── 9-Authors.md
├── 5-Migrating.md
├── 7-Tips.md
├── 3-Config.md
├── 6-FAQ.md
├── 1-Getting-started.md
└── 4-Handlers.md
├── handlers
├── files.clj
├── slack.clj
├── postal.clj
├── sockets.clj
├── consoles.cljc
└── open_telemetry.clj
├── doc
└── cljdoc.edn
├── main
├── shadow-cljs.sh
├── jaeger.sh
├── test
│ └── taoensso
│ │ └── graal_tests.clj
├── bb.edn
├── .gitignore
├── public
│ └── index.html
├── shadow-cljs.edn
├── bb
│ └── graal_tests.clj
├── resources
│ └── docs
│ │ ├── error!.txt
│ │ ├── signal!.txt
│ │ ├── log!.txt
│ │ ├── event!.txt
│ │ ├── catch-to-error!.txt
│ │ ├── signal-creators.txt
│ │ ├── signal-content.txt
│ │ ├── environmental-config.txt
│ │ ├── signal-options.txt
│ │ ├── spy!.txt
│ │ └── trace!.txt
├── src
│ └── taoensso
│ │ └── telemere
│ │ ├── slack.clj
│ │ ├── tools_logging.clj
│ │ ├── postal.clj
│ │ ├── sockets.clj
│ │ ├── consoles.cljc
│ │ ├── streams.clj
│ │ ├── timbre.cljc
│ │ └── files.clj
└── project.clj
├── FUNDING.yml
├── imgs
├── telemere.fig
├── handler-output-clj-file.png
├── handler-output-cljs-console.png
├── handler-output-cljs-console-raw.png
└── telemere-logo.svg
├── install.sh
├── slf4j
├── resources
│ └── META-INF
│ │ └── services
│ │ └── org.slf4j.spi.SLF4JServiceProvider
├── .gitignore
├── project.clj
└── src
│ ├── java
│ └── com
│ │ └── taoensso
│ │ └── telemere
│ │ └── slf4j
│ │ ├── TelemereLoggerFactory.java
│ │ ├── TelemereServiceProvider.java
│ │ └── TelemereLogger.java
│ └── taoensso
│ └── telemere
│ └── slf4j.clj
├── SECURITY.md
├── .github
└── workflows
│ ├── cljs-tests.yml
│ ├── clj-tests.yml
│ └── graal-tests.yml
├── LICENSE.txt
└── examples.cljc
/wiki/.gitignore:
--------------------------------------------------------------------------------
1 | README.md
2 |
--------------------------------------------------------------------------------
/handlers/files.clj:
--------------------------------------------------------------------------------
1 | ../main/src/taoensso/telemere/files.clj
--------------------------------------------------------------------------------
/handlers/slack.clj:
--------------------------------------------------------------------------------
1 | ../main/src/taoensso/telemere/slack.clj
--------------------------------------------------------------------------------
/doc/cljdoc.edn:
--------------------------------------------------------------------------------
1 | {:cljdoc/docstring-format :plaintext}
2 |
3 |
--------------------------------------------------------------------------------
/handlers/postal.clj:
--------------------------------------------------------------------------------
1 | ../main/src/taoensso/telemere/postal.clj
--------------------------------------------------------------------------------
/handlers/sockets.clj:
--------------------------------------------------------------------------------
1 | ../main/src/taoensso/telemere/sockets.clj
--------------------------------------------------------------------------------
/handlers/consoles.cljc:
--------------------------------------------------------------------------------
1 | ../main/src/taoensso/telemere/consoles.cljc
--------------------------------------------------------------------------------
/main/shadow-cljs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | npx shadow-cljs watch main
3 |
--------------------------------------------------------------------------------
/handlers/open_telemetry.clj:
--------------------------------------------------------------------------------
1 | ../main/src/taoensso/telemere/open_telemetry.clj
--------------------------------------------------------------------------------
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ptaoussanis
2 | custom: "https://www.taoensso.com/clojure"
3 |
--------------------------------------------------------------------------------
/imgs/telemere.fig:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoensso/telemere/HEAD/imgs/telemere.fig
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd main; lein install; cd ..;
4 | cd slf4j; lein install; cd ..;
5 |
6 |
--------------------------------------------------------------------------------
/imgs/handler-output-clj-file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoensso/telemere/HEAD/imgs/handler-output-clj-file.png
--------------------------------------------------------------------------------
/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider:
--------------------------------------------------------------------------------
1 | com.taoensso.telemere.slf4j.TelemereServiceProvider
--------------------------------------------------------------------------------
/imgs/handler-output-cljs-console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoensso/telemere/HEAD/imgs/handler-output-cljs-console.png
--------------------------------------------------------------------------------
/imgs/handler-output-cljs-console-raw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoensso/telemere/HEAD/imgs/handler-output-cljs-console-raw.png
--------------------------------------------------------------------------------
/main/jaeger.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker run --rm \
4 | -p 16686:16686 \
5 | -p 4318:4318 \
6 | jaegertracing/all-in-one:latest
7 |
8 | open "http://localhost:16686"
9 |
--------------------------------------------------------------------------------
/wiki/README.md:
--------------------------------------------------------------------------------
1 | # Attention!
2 |
3 | This wiki is designed for viewing from [here](../../../wiki)!
4 |
5 | Viewing from GitHub's file browser will result in **broken links**.
6 |
--------------------------------------------------------------------------------
/main/test/taoensso/graal_tests.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.graal-tests
2 | (:require [taoensso.telemere :as telemere])
3 | (:gen-class))
4 |
5 | (defn -main [& args] (println "Namespace loaded successfully"))
6 |
--------------------------------------------------------------------------------
/slf4j/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml*
2 | .lein*
3 | .nrepl-port
4 | *.jar
5 | *.class
6 | .env
7 | .DS_Store
8 | /lib/
9 | /classes/
10 | /target/
11 | /checkouts/
12 | /logs/
13 | /.clj-kondo/.cache
14 | .idea/
15 | *.iml
16 |
--------------------------------------------------------------------------------
/main/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["bb"]
2 | :tasks
3 | {:requires ([graal-tests])
4 | graal-tests
5 | {:doc "Run Graal native-image tests"
6 | :task
7 | (do
8 | (graal-tests/uberjar)
9 | (graal-tests/native-image)
10 | (graal-tests/run-tests))}}}
11 |
--------------------------------------------------------------------------------
/main/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml*
2 | .lein*
3 | .nrepl-port
4 | *.jar
5 | *.class
6 | .env
7 | .DS_Store
8 | /lib/
9 | /classes/
10 | /target/
11 | /checkouts/
12 | /logs/
13 | /test/logs/
14 | /.clj-kondo/.cache
15 | .idea/
16 | *.iml
17 | /wiki/.git
18 | .shadow-cljs/
19 | public/js/
20 | out/
21 |
--------------------------------------------------------------------------------
/main/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Telemere/shadow-cljs
6 |
7 |
8 |
9 | shadow-cljs dev HTTP server
10 | For: taoensso.telemere
11 |
12 |
13 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security policy
2 |
3 | ## Advisories
4 |
5 | All security advisories will be posted [on GitHub](https://github.com/taoensso/telemere/security/advisories).
6 |
7 | ## Reporting a vulnerability
8 |
9 | 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).
10 |
11 | Thank you!
12 |
13 | \- [Peter Taoussanis](https://www.taoensso.com)
--------------------------------------------------------------------------------
/main/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {;;:lein true
2 | :source-paths ["src" "test"]
3 | :dependencies
4 | [[com.taoensso/encore "3.112.0"]
5 | [cider/cider-nrepl "0.47.0"]
6 | [binaryage/devtools "1.0.7"]]
7 |
8 | :nrepl
9 | {:port 7887
10 | :middleware
11 | [cider.nrepl/cider-middleware]}
12 |
13 | :dev-http {8090 {:root "public"}}
14 | :builds
15 | {:main
16 | {:target :browser
17 | :output-dir "public/js"
18 | :modules {:main {:entries [taoensso.telemere]}}
19 | :preloads [devtools.preload]}
20 |
21 | :tests
22 | {:target :node-test
23 | :output-to "target/tests.js"
24 | :ns-regexp "-tests$"
25 | :autorun true}}}
26 |
--------------------------------------------------------------------------------
/wiki/Home.md:
--------------------------------------------------------------------------------
1 | # Content
2 |
3 | See the **Pages menu to the right** for content 👉
4 |
5 | # Attention!
6 |
7 | This wiki is designed for viewing from GitHub's **Wiki** UI.
8 |
9 | Viewing from GitHub's file browser will result in **broken links**.
10 |
11 | # Please report errors
12 |
13 | I'm currently maintaining a lot of documentation! Typos, broken links, or obsolete info *will* sneak in from time-to-time.
14 |
15 | If you run into something that looks like an error, please [report](../issues) it! 🙏
16 |
17 | Thank you! \- [Peter Taoussanis](https://www.taoensso.com)
18 |
19 | # Contributions welcome
20 |
21 | **PRs very welcome** to help improve this documentation!
22 |
23 | See the [wiki](../tree/master/wiki) folder in the main repo for the relevant files.
--------------------------------------------------------------------------------
/.github/workflows/cljs-tests.yml:
--------------------------------------------------------------------------------
1 | name: Cljs tests
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | tests:
6 | strategy:
7 | matrix:
8 | java: ['21']
9 | os: [ubuntu-latest]
10 | runs-on: ${{ matrix.os }}
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-java@v4
14 | with:
15 | distribution: 'corretto'
16 | java-version: ${{ matrix.java }}
17 | - uses: DeLaGuardo/setup-clojure@12.5
18 | with:
19 | lein: latest
20 | - uses: actions/cache@v4
21 | id: cache-deps
22 | with:
23 | path: ~/.m2/repository
24 | key: deps-${{ hashFiles('main/project.clj') }}
25 | restore-keys: deps-
26 | - run: lein test-cljs
27 | working-directory: main
28 |
--------------------------------------------------------------------------------
/.github/workflows/clj-tests.yml:
--------------------------------------------------------------------------------
1 | name: Clj tests
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | tests:
6 | strategy:
7 | matrix:
8 | java: ['17', '19', '21']
9 | os: [ubuntu-latest]
10 | runs-on: ${{ matrix.os }}
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-java@v4
14 | with:
15 | distribution: 'corretto'
16 | java-version: ${{ matrix.java }}
17 | - uses: DeLaGuardo/setup-clojure@12.5
18 | with:
19 | lein: latest
20 | - uses: actions/cache@v4
21 | id: cache-deps
22 | with:
23 | path: ~/.m2/repository
24 | key: deps-${{ hashFiles('main/project.clj') }}
25 | restore-keys: deps-
26 | - run: lein test-clj
27 | working-directory: main
28 |
--------------------------------------------------------------------------------
/slf4j/project.clj:
--------------------------------------------------------------------------------
1 | (defproject com.taoensso/telemere-slf4j "1.2.1"
2 | :author "Peter Taoussanis "
3 | :description "Telemere backend/provider for SLF4J API v2"
4 | :url "https://www.taoensso.com/telemere"
5 |
6 | :license
7 | {:name "Eclipse Public License - v 1.0"
8 | :url "https://www.eclipse.org/legal/epl-v10.html"}
9 |
10 | :scm {:name "git" :url "https://github.com/taoensso/telemere"}
11 |
12 | :java-source-paths ["src/java"]
13 | :javac-options ["--release" "8" "-g"] ; Support Java >= v8
14 | :dependencies []
15 |
16 | :profiles
17 | {:provided
18 | {:dependencies
19 | [[org.clojure/clojure "1.12.3"]
20 | [org.slf4j/slf4j-api "2.0.17"]
21 | [com.taoensso/telemere "1.2.1"]]}
22 |
23 | :dev
24 | {:plugins
25 | [[lein-pprint "1.3.2"]
26 | [lein-ancient "0.7.0"]]}}
27 |
28 | :aliases
29 | {"deploy-lib" ["do" #_["build-once"] ["deploy" "clojars"] ["install"]]})
30 |
--------------------------------------------------------------------------------
/.github/workflows/graal-tests.yml:
--------------------------------------------------------------------------------
1 | name: Graal tests
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | tests:
6 | strategy:
7 | matrix:
8 | java: ['17']
9 | os: [ubuntu-latest, macOS-latest, windows-latest]
10 |
11 | runs-on: ${{ matrix.os }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: graalvm/setup-graalvm@v1
15 | with:
16 | version: 'latest'
17 | java-version: ${{ matrix.java }}
18 | components: 'native-image'
19 | github-token: ${{ secrets.GITHUB_TOKEN }}
20 |
21 | - uses: DeLaGuardo/setup-clojure@12.5
22 | with:
23 | lein: latest
24 | bb: latest
25 |
26 | - uses: actions/cache@v4
27 | with:
28 | path: ~/.m2/repository
29 | key: deps-${{ hashFiles('main/project.clj') }}
30 | restore-keys: deps-
31 |
32 | - name: Run Graal tests
33 | run: bb graal-tests
34 | working-directory: main
35 |
36 | # - name: Run Babashka tests
37 | # run: bb bb-tests
38 | # working-directory: main
39 |
--------------------------------------------------------------------------------
/wiki/2-Architecture.md:
--------------------------------------------------------------------------------
1 | Telemere's key function is to help:
2 |
3 | 1. **Capture information** in your running Clojure/Script programs, and
4 | 2. **Facilitate processing** of that information to support **insight**.
5 |
6 | Its basic tools:
7 |
8 | 1. **Signal creators** to conditionally *create* signal maps at points in your code.
9 | 2. **Signal handlers** to conditionally *handle* those signal maps (analyse, write to
10 | console/file/queue/db, etc.).
11 |
12 | So you *call* a *signal creator* to (conditionally) create a *signal* (map) which is then dispatched to registered _signal handlers_ for (conditional) handling.
13 |
14 | This flow is visualized below:
15 |
16 |
17 |
18 | - `A/sync queue` semantics are specified via [handler dispatch options](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options).
19 | - The shared **call transform** cache is super useful when doing signal transformations that are expensive and/or involve side effects (like syncing with another service/db to get a unique tx id, etc.).
--------------------------------------------------------------------------------
/main/bb/graal_tests.clj:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bb
2 |
3 | (ns graal-tests
4 | (:require
5 | [clojure.string :as str]
6 | [babashka.fs :as fs]
7 | [babashka.process :refer [shell]]))
8 |
9 | (defn uberjar []
10 | (let [command "lein with-profiles +graal-tests uberjar"
11 | command
12 | (if (fs/windows?)
13 | (if (fs/which "lein")
14 | command
15 | ;; Assume PowerShell powershell module
16 | (str "powershell.exe -command " (pr-str command)))
17 | command)]
18 |
19 | (shell command)))
20 |
21 | (defn executable [dir name]
22 | (-> (fs/glob dir (if (fs/windows?) (str name ".{exe,bat,cmd}") name))
23 | first
24 | fs/canonicalize
25 | str))
26 |
27 | (defn native-image []
28 | (let [graalvm-home (System/getenv "GRAALVM_HOME")
29 | bin-dir (str (fs/file graalvm-home "bin"))]
30 | (shell (executable bin-dir "gu") "install" "native-image")
31 | (shell (executable bin-dir "native-image")
32 | "--features=clj_easy.graal_build_time.InitClojureClasses"
33 | "--no-fallback" "-jar" "target/graal-tests.jar" "graal_tests")))
34 |
35 | (defn run-tests []
36 | (let [{:keys [out]} (shell {:out :string} (executable "." "graal_tests"))]
37 | (assert (str/includes? out "loaded") out)
38 | (println "Native image works!")))
39 |
--------------------------------------------------------------------------------
/main/resources/docs/error!.txt:
--------------------------------------------------------------------------------
1 | "Error" signal creator, emphasizing (optional id) + error (Exception, etc.).
2 |
3 | Default kind: `:error`
4 | Default level: `:error`
5 | Returns:
6 | ALWAYS (unconditionally) returns the given error, so can conveniently be
7 | wrapped by `throw`: (throw (error! (ex-info ...)), etc.
8 |
9 | Examples:
10 |
11 | (throw (error! (ex-info "MyEx" {}))) ; %> {:kind :error, :level :error, :error ...}
12 | (throw (error! ::my-id (ex-info "MyEx" {}))) ; %> {... :id ::my-id ...}
13 | (throw
14 | (error!
15 | {:let [x "x"] ; Available to `:data` and `:msg`
16 | :data {:x x}
17 | :msg ["My message:" x]}
18 |
19 | (ex-info "MyEx" {}))) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
20 |
21 | Tips:
22 |
23 | - Test using `with-signal`: (with-signal (error! ...)).
24 | - Supports the same options [2] as other signals [1].
25 |
26 | - `error` arg is a platform error (`java.lang.Throwable` or `js/Error`).
27 |
28 | ----------------------------------------------------------------------
29 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
30 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
31 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
32 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
33 |
--------------------------------------------------------------------------------
/main/resources/docs/signal!.txt:
--------------------------------------------------------------------------------
1 | Low-level "generic" signal creator for creating signals of any "kind".
2 | Takes a single map of options [2] with compile-time keys.
3 |
4 | Default kind: `:generic` (feel free to change!)
5 | Default level: `:info`
6 | Returns:
7 | - If given `:run` form: unconditionally returns run value, or rethrows run error.
8 | - Otherwise: returns true iff signal was created (allowed by filtering).
9 |
10 | When filtering conditions are met [4], creates a Telemere signal [3] and
11 | dispatches it to registered handlers for processing (e.g. writing to
12 | console/file/queue/db, etc.).
13 |
14 | Generic signals are fairly low-level and useful mostly for library authors or
15 | advanced users writing their own wrapper macros. NB see `keep-callsite` for
16 | preserving callsite coords when wrapping Telemere macros like `signal!`.
17 |
18 | Regular users will typically prefer one of the higher-level signal creators
19 | optimized for ease-of-use in common cases [1].
20 |
21 | Tips:
22 |
23 | - Test using `with-signal`: (with-signal (signal! ...)).
24 | - Supports the same options [2] as other signals [1].
25 |
26 | ----------------------------------------------------------------------
27 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
28 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
29 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
30 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
31 |
--------------------------------------------------------------------------------
/main/resources/docs/log!.txt:
--------------------------------------------------------------------------------
1 | "Log" signal creator, emphasizing (optional level) + message.
2 |
3 | Default kind: `:log`
4 | Default level: `:info`
5 | Returns:
6 | - For `log!` variant: nil, unconditionally.
7 | - For `log!?` variant: true iff signal was created (allowed by filtering).
8 |
9 | When filtering conditions are met [4], creates a Telemere signal [3] and
10 | dispatches it to registered handlers for processing (e.g. writing to
11 | console/file/queue/db, etc.).
12 |
13 | Examples:
14 |
15 | (log! "My msg") ; %> {:kind :log, :level :info, :id ::my-id ...}
16 | (log! :warn "My msg") ; %> {... :level :warn ...}
17 | (log!
18 | {:let [x "x"] ; Available to `:data` and `:msg`
19 | :data {:x x}}
20 |
21 | ["My msg:" x]) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
22 |
23 | Tips:
24 |
25 | - Test using `with-signal`: (with-signal (log! ...)).
26 | - Supports the same options [2] as other signals [1].
27 |
28 | - `log!` and `event!` are both good default/general-purpose signal creators.
29 | - `log!` emphasizes messages, while `event!` emphasizes ids.
30 |
31 | - `msg` arg may be a string, or vector of strings to join with `\space`.
32 | - See also `msg-splice`, `msg-skip` utils.
33 |
34 | ----------------------------------------------------------------------
35 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
36 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
37 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
38 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
39 |
--------------------------------------------------------------------------------
/main/resources/docs/event!.txt:
--------------------------------------------------------------------------------
1 | "Event" signal creator, emphasizing id + (optional level).
2 |
3 | Default kind: `:event`
4 | Default level: `:info`
5 | Returns:
6 | - For `event!` variant: nil, unconditionally.
7 | - For `event!?` variant: true iff signal was created (allowed by filtering).
8 |
9 | When filtering conditions are met [4], creates a Telemere signal [3] and
10 | dispatches it to registered handlers for processing (e.g. writing to
11 | console/file/queue/db, etc.).
12 |
13 | Examples:
14 |
15 | (event! ::my-id) ; %> {:kind :event, :level :info, :id ::my-id ...}
16 | (event! ::my-id :warn) ; %> {... :level :warn ...}
17 | (event! ::my-id
18 | {:let [x "x"] ; Available to `:data` and `:msg`
19 | :data {:x x}
20 | :msg ["My msg:" x]}) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
21 |
22 | Tips:
23 |
24 | - Test using `with-signal`: (with-signal (event! ...)).
25 | - Supports the same options [2] as other signals [1].
26 |
27 | - `log!` and `event!` are both good default/general-purpose signal creators.
28 | - `log!` emphasizes messages, while `event!` emphasizes ids.
29 |
30 | - Has a different 2-arity arg order to all other signals!
31 | Mnemonic: the arg that's typically larger is *always* in the rightmost
32 | position, and for `event!` that's the `level-or-opts` arg.
33 |
34 | ----------------------------------------------------------------------
35 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
36 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
37 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
38 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
39 |
--------------------------------------------------------------------------------
/main/resources/docs/catch-to-error!.txt:
--------------------------------------------------------------------------------
1 | ALWAYS (unconditionally) executes given `run` form and:
2 |
3 | Default kind: `:error`
4 | Default level: `:error`
5 | Returns:
6 | - If given `run` form succeeds: returns the form's result.
7 | - If given `run` form throws ANYTHING:
8 | Calls `error!` with the thrown error and given signal options [2], then
9 | either returns given (:catch-val opts), or rethrows.
10 |
11 | Just a convenience util. For more flexibility use your own `try/catch`.
12 | See `taoensso.encore/try*` for easily catching cross-platform errors.
13 |
14 | Examples:
15 |
16 | (catch->error! (/ 1 0)) ; %> {:kind :error, :level :error, :error ...}
17 | (catch->error! ::my-id (/ 1 0)) ; %> {... :id ::my-id ...}
18 | (catch->error!
19 | {:let [x "x"] ; Available to `:data` and `:msg`
20 | :data {:x x}
21 | :msg ["My msg:" x]
22 | :catch-val "Return value iff form throws"}
23 |
24 | (/ 1 0)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
25 |
26 | Tips:
27 |
28 | - Test using `with-signal`: (with-signal (catch->error! ...)).
29 | - Supports the same options [2] as other signals [1].
30 |
31 | - Useful for preventing errors from going unnoticed in futures, callbacks,
32 | agent actions, etc.!: (future (catch->error ::my-future (do-something)))
33 |
34 | See also `error!`.
35 |
36 | ----------------------------------------------------------------------
37 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
38 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
39 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
40 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
41 |
--------------------------------------------------------------------------------
/main/resources/docs/signal-creators.txt:
--------------------------------------------------------------------------------
1 | Call a Telemere signal creator to conditionally create a signal at that callsite.
2 |
3 | When filtering conditions are met [4], the call creates a Telemere signal [3]
4 | and dispatches it to registered handlers for processing (e.g. writing to
5 | console/file/queue/db, etc.).
6 |
7 | Telemere doesn't make a hard distinction between different kinds of signals
8 | (log, event, error, etc.) - they're all just plain Clojure/Script maps with
9 | various keys:
10 |
11 | - All signal creators offer the same options [2], and
12 | - All signal kinds can contain the same content [3]
13 |
14 | Creators vary only in in their default `:kind` value and call APIs (expected
15 | args and return values), making them more/less convenient for certain use cases:
16 |
17 | `log!` ------------- ?level + msg => nil
18 | `event!` ----------- id + ?level => nil
19 | `trace!` ----------- ?id + run => run result (value or throw)
20 | `spy!` ------------- ?level + run => run result (value or throw)
21 | `error!` ----------- ?id + error => given error
22 | `catch->error!` ---- ?id + run => run value or ?catch-val
23 | `uncaught->error!` - ?id => nil
24 | `signal!` ---------- opts => allowed? / run result (value or throw)
25 |
26 | - `log!` and `event!` are both good default/general-purpose signal creators.
27 | - `log!` emphasizes messages, while `event!` emphasizes ids.
28 | - `signal!` is the generic creator, and is used by all the others.
29 |
30 | ----------------------------------------------------------------------
31 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
32 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
33 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
34 |
--------------------------------------------------------------------------------
/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLoggerFactory.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2004-2011 QOS.ch
3 | * All rights reserved.
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining
6 | * a copy of this software and associated documentation files (the
7 | * "Software"), to deal in the Software without restriction, including
8 | * without limitation the rights to use, copy, modify, merge, publish,
9 | * distribute, sublicense, and/or sell copies of the Software, and to
10 | * permit persons to whom the Software is furnished to do so, subject to
11 | * the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be
14 | * included in all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | *
24 | */
25 | package com.taoensso.telemere.slf4j;
26 | // Based on `org.slf4j.simple.SimpleLoggerFactory`
27 |
28 | import java.util.concurrent.ConcurrentHashMap;
29 | import java.util.concurrent.ConcurrentMap;
30 |
31 | import org.slf4j.Logger;
32 | import org.slf4j.ILoggerFactory;
33 |
34 | public class TelemereLoggerFactory implements ILoggerFactory {
35 |
36 | ConcurrentMap loggerMap;
37 |
38 | public TelemereLoggerFactory() {
39 | loggerMap = new ConcurrentHashMap<>();
40 | TelemereLogger.lazyInit();
41 | }
42 |
43 | public Logger getLogger(String name) { return loggerMap.computeIfAbsent(name, this::createLogger); }
44 | protected Logger createLogger(String name) { return new TelemereLogger(name); }
45 | protected void reset() { loggerMap.clear(); }
46 | }
47 |
--------------------------------------------------------------------------------
/main/resources/docs/signal-content.txt:
--------------------------------------------------------------------------------
1 | Telemere signals are maps with {:keys [inst id ns level data msg_ ...]},
2 | though they can be modified by call and/or handler transform (xfns).
3 |
4 | Default signal keys:
5 |
6 | `:schema` ------ Int version of signal schema (current: 1)
7 | `:inst` -------- Platform instant [1] when signal was created, monotonicity depends on system clock
8 | `:ns` ---------- ?str namespace of signal callsite
9 | `:coords` ------ ?[line column] of signal callsite
10 |
11 | `:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy :slf4j :tools-logging ...}
12 | `:level` ------- Signal level ∈ #{ :trace :debug :info :warn :error :fatal :report ...}
13 | `:id` ---------- Signal callsite ?id (usu. keyword) (common to all signals created at callsite, contrast with `:uid`)
14 | `:uid` --------- Signal instance ?id (usu. string) (unique to each signal created at callsite when tracing, contrast with `:id`)
15 |
16 | `:msg_` -------- Arb app-level message ?str given to signal creator - may be a delay, always use `force` to unwrap!
17 | `:data` -------- Arb app-level data ?val (usu. a map) given to signal creator
18 | `:error` ------- Arb app-level platform ?error [2] given to signal creator
19 |
20 | `:run-form` ---- Unevaluated ?form given to signal creator as `:run`
21 | `:run-val` ----- Successful return ?val of `:run` ?form
22 | `:run-nsecs` --- ?int nanosecs runtime of `:run` ?form
23 | `:end-inst` ---- Platform ?instant [1] when `:run` ?form completed
24 |
25 | `:parent` ------ ?{:keys [id uid]} of parent signal, present in nested signals when tracing
26 | `:root` -------- ?{:keys [id uid]} of root signal, present in nested signals when tracing
27 | `:ctx` --------- ?val of `*ctx*` (arb app-level state) when signal was created
28 |
29 | `:host` -------- (Clj only) {:keys [name ip]} info for network host
30 | `:thread` ------ (Clj only) {:keys [name id group]} info for thread that created signal
31 |
32 | `:sample` ------ Sample ?rate ∈ℝ[0,1] for combined call AND handler sampling (0.75 => allow 75% of signals, nil => allow all)
33 |
34 | ---------- Other arb app-level ?kvs given to signal creator. Typically NOT included
35 | in handler output, so a great way to provide custom data/opts for use
36 | (only) by custom transforms/handlers.
37 |
38 | If anything is unclear, please ping me (@ptaoussanis) so that I can improve these docs!
39 |
40 | [1] `java.time.Instant` or `js/Date`
41 | [2] `java.lang.Throwable` or `js/Error`
42 |
--------------------------------------------------------------------------------
/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereServiceProvider.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2004-2011 QOS.ch
3 | * All rights reserved.
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining
6 | * a copy of this software and associated documentation files (the
7 | * "Software"), to deal in the Software without restriction, including
8 | * without limitation the rights to use, copy, modify, merge, publish,
9 | * distribute, sublicense, and/or sell copies of the Software, and to
10 | * permit persons to whom the Software is furnished to do so, subject to
11 | * the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be
14 | * included in all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | *
24 | */
25 | package com.taoensso.telemere.slf4j;
26 | // Based on `org.slf4j.simple.SimpleServiceProvider`
27 |
28 | import org.slf4j.ILoggerFactory;
29 | import org.slf4j.IMarkerFactory;
30 | import org.slf4j.helpers.BasicMarkerFactory;
31 | import org.slf4j.helpers.BasicMDCAdapter;
32 | import org.slf4j.spi.MDCAdapter;
33 | import org.slf4j.spi.SLF4JServiceProvider;
34 |
35 | public class TelemereServiceProvider implements SLF4JServiceProvider {
36 |
37 | public static String REQUESTED_API_VERSION = "2.0.99"; // Should not be final
38 |
39 | private ILoggerFactory loggerFactory;
40 | private IMarkerFactory markerFactory;
41 | private MDCAdapter mdcAdapter;
42 |
43 | public ILoggerFactory getLoggerFactory() { return loggerFactory; }
44 | @Override public IMarkerFactory getMarkerFactory() { return markerFactory; }
45 | @Override public MDCAdapter getMDCAdapter() { return mdcAdapter; }
46 | @Override public String getRequestedApiVersion() { return REQUESTED_API_VERSION; }
47 | @Override
48 | public void initialize() {
49 | loggerFactory = new TelemereLoggerFactory();
50 | markerFactory = new BasicMarkerFactory();
51 | mdcAdapter = new BasicMDCAdapter();
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/wiki/8-Community.md:
--------------------------------------------------------------------------------
1 | My plan for Telemere is to offer a **stable core of limited scope**, then to focus on making it as easy for the **community** to write additional stuff like handlers, transforms, and utils.
2 |
3 | [PRs](../wiki#contributions-welcome) **very welcome** to add links to this page!
4 |
5 | If you spot issues with any linked resources, please **contact the relevant authors** to let them know! Thank you! 🙏 - [Peter](https://www.taoensso.com)
6 |
7 | # Learning
8 |
9 | Includes videos, tutorials, demo projects, etc.
10 |
11 | | Type | Description |
12 | | ------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13 | | Support | [Official Slack channel](https://www.taoensso.com/telemere/slack) for questions, help, etc. |
14 | | Support | [Official GitHub issues](https://github.com/taoensso/telemere/issues) for questions, help, bug reports, PRs, etc. |
15 | | Example | [Gist](https://gist.github.com/ptaoussanis/f8a80f85d3e0f89b307a470ce6e044b5) showing use with [Bling](https://github.com/paintparty/bling) (2024-12-23) |
16 | | Example | [Gist](https://gist.github.com/xlfe/e9e2cf23bd1dddcbb2fbd77ce31dcc8b) showing use with **Google Cloud Platform** (GCP) (2024-10-13) |
17 | | Study | [YouTube learning session](https://www.youtube.com/watch?v=uyApiNg6h7Y) by [Los Angeles Clojure Users Group](https://www.meetup.com/los-angeles-clojure-users-group/) (107 mins) (2024-06-12) |
18 | | Demo | [Official YouTube demo](https://www.youtube.com/watch?v=-L9irDG8ysM) for Telemere's launch (24 mins) (2024-04-18) |
19 |
20 | # Handlers and tools
21 |
22 | Includes libraries or examples for handlers (see [Writing handlers](./4-Handlers#writing-handlers)), transforms, handler utils (e.g. formatters), tools for analyzing signals, etc.
23 |
24 | | Type | Description |
25 | | ------- | :------------------------------------------------------------ |
26 | | Handler | [Axiom.co](https://github.com/marksto/telemere.axiom) handler |
27 | | - | Your link here? [PRs](../wiki#contributions-welcome) welcome! |
28 |
--------------------------------------------------------------------------------
/wiki/9-Authors.md:
--------------------------------------------------------------------------------
1 | Are you a library author/maintainer that's considering **using Telemere in your library**?
2 |
3 | You have **a few options** below-
4 |
5 | # Options
6 |
7 | ## Modern logging facade
8 |
9 | [Trove](https://www.taoensso.com/trove) is a minimal, modern alternative to [tools.logging](https://github.com/clojure/tools.logging) that supports all of Telemere's structured logging and rich filtering features.
10 |
11 | Basically:
12 |
13 | 1. You include the (very small) Trove dependency with your library
14 | 2. Your library logs using the [Trove API](https://github.com/taoensso/trove#to-choose-a-backend)
15 | 3. Your users then [choose](https://github.com/taoensso/trove#to-choose-a-backend) their preferred backend (Telemere, etc.)
16 |
17 | This would be my first recommendation, and is what I'm planning to use for future updates to [Sente](https://www.taoensso.com/sente), [Carmine](https://www.taoensso.com/carmine), etc.
18 |
19 | ## Traditional logging facade (basic logging only)
20 |
21 | Many libraries need only basic logging. In these cases it can be beneficial to do your logging through a common traditional logging facade like [tools.logging](https://github.com/clojure/tools.logging) or [SLF4J](https://www.slf4j.org/).
22 |
23 | Though these'll limit you to basic features (e.g. no structured logging or [rich filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters)).
24 |
25 | ## Telemere as a transitive dependency
26 |
27 | You could just include [Telemere](https://clojars.org/com.taoensso/telemere) in your **library's dependencies**. Your library (and users) will then have access to the full Telemere API.
28 |
29 | Telemere's [default config](./1-Getting-started#default-config) is sensible (with println-like console output), so your users are unlikely to need to configure or interact with Telemere much unless they choose to.
30 |
31 | The most common thing users may want to do is **adjust the minimum level** of signals created by your library. You can help make this as easy as possible by adding a util to your library:
32 |
33 | ```clojure
34 | (defn set-min-log-level!
35 | "Sets Telemere's minimum level for namespaces.
36 | This will affect all signals (logs) created by .
37 |
38 | Possible minimum levels (from most->least verbose):
39 | #{:trace :debug :info :warn :error :fatal :report}.
40 |
41 | The default minimum level is `:warn`."
42 | [min-level]
43 | (tel/set-min-level! nil "my-lib-ns(.*)" min-level)
44 | true)
45 |
46 | (defonce ^:private __set-default-log-level (set-min-log-level! :warn))
47 | ```
48 |
49 | This way your users can easily disable, decrease, or increase signal output from your library without even needing to touch Telemere or to be aware of its existence.
--------------------------------------------------------------------------------
/main/resources/docs/environmental-config.txt:
--------------------------------------------------------------------------------
1 | Telemere supports extensive environmental config via JVM properties,
2 | environment variables, or classpath resources.
3 |
4 | Environmental filter config includes:
5 |
6 | 1. Minimum level (see signal `:level`):
7 | a. JVM property: `taoensso.telemere.rt-min-level`
8 | b. Env variable: `TAOENSSO_TELEMERE_RT_MIN_LEVEL`
9 | c. Classpath resource: `taoensso.telemere.rt-min-level`
10 |
11 | 2. Namespace filter (see signal `:ns`):
12 | a. JVM property: `taoensso.telemere.rt-ns-filter`
13 | b. Env variable: `TAOENSSO_TELEMERE_RT_NS_FILTER`
14 | c. Classpath resource: `taoensso.telemere.rt-ns-filter`
15 |
16 | 3. Id filter (see signal `:id`):
17 | a. JVM property: `taoensso.telemere.rt-id-filter`
18 | b. Env variable: `TAOENSSO_TELEMERE_RT_ID_FILTER`
19 | c. Classpath resource: `taoensso.telemere.rt-id-filter`
20 |
21 | 4. Kind filter (signal `:kind`):
22 | a. JVM property: `taoensso.telemere.rt-kind-filter`
23 | b. Env variable: `TAOENSSO_TELEMERE_RT_KIND_FILTER`
24 | c. Classpath resource: `taoensso.telemere.rt-kind-filter`
25 |
26 | Config values are parsed as edn, examples:
27 |
28 | `taoensso.telemere.rt-min-level` => ":info"
29 | `TAOENSSO_TELEMERE_RT_NS_FILTER` => "{:disallow \"taoensso.*\"}"
30 | `taoensso.telemere.rt-id-filter.cljs` => "#{:my-id1 :my-id2}"
31 | `TAOENSSO_TELEMERE_RT_KIND_FILTER_CLJ` => "nil"
32 |
33 | Runtime vs compile-time filters
34 |
35 | The above filters (1..4) all apply at RUNTIME ("rt").
36 | This is typically what you want, since it allows you to freely adjust filtering
37 | (making it less OR MORE permissive) through later API calls like `set-min-level!`.
38 |
39 | As an advanced option, you can instead/additionally ELIDE (entirely omit) filtered
40 | callsites at COMPILE-TIME ("ct") by replacing "rt"->"ct" / "RT"->"CT" in the config
41 | ids above. Compile-time filters CANNOT be made MORE permissive at runtime.
42 |
43 | Tips:
44 |
45 | - The above config ids will affect both Clj AND Cljs.
46 | For platform-specific filters, use
47 | ".clj" / "_CLJ" or
48 | ".cljs" / "_CLJS" suffixes instead.
49 | e.g. "taoensso.telemere.rt-min-level.cljs".
50 |
51 | - To get the right edn syntax, first set your runtime filters using the
52 | standard utils (`set-min-level!`, etc.). Then call `get-filters` and
53 | serialize the relevant parts to edn with `pr-str`.
54 |
55 | - All environmental config uses `get-env` underneath.
56 | See the `get-env` docstring for more/advanced details.
57 |
58 | - Classpath resources are files accessible on your project's
59 | classpath. This usually includes files in your project's
60 | `resources/` dir.
61 |
--------------------------------------------------------------------------------
/main/src/taoensso/telemere/slack.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.telemere.slack
2 | "Telemere -> Slack handler using `clj-slack`,
3 | Ref. "
4 | (:require
5 | [taoensso.truss :as truss]
6 | [taoensso.encore :as enc]
7 | [taoensso.telemere.utils :as utils]
8 | [clj-slack.core :as slack]
9 | [clj-slack.chat :as slack.chat]))
10 |
11 | (comment
12 | (require '[taoensso.telemere :as tel])
13 | (remove-ns (symbol (str *ns*)))
14 | (:api (enc/interns-overview)))
15 |
16 | (def default-dispatch-opts
17 | {:min-level :info
18 | :limit
19 | [[5 (enc/msecs :mins 1)]
20 | [10 (enc/msecs :mins 15)]
21 | [15 (enc/msecs :hours 1)]
22 | [30 (enc/msecs :hours 6)]]})
23 |
24 | (defn handler:slack
25 | "Alpha, subject to change.
26 |
27 | Needs `clj-slack`, Ref. .
28 |
29 | Returns a signal handler that:
30 | - Takes a Telemere signal (map).
31 | - Writes the signal as a string to specified Slack channel.
32 |
33 | Can output signals as human or machine-readable (edn, JSON) strings.
34 |
35 | Default handler dispatch options (override when calling `add-handler!`):
36 | `:min-level` - `:info`
37 | `:limit` -
38 | [[5 (enc/msecs :mins 1)] ; Max 5 posts in 1 min
39 | [10 (enc/msecs :mins 15)] ; Max 10 posts in 15 mins
40 | [15 (enc/msecs :hours 1)] ; Max 15 posts in 1 hour
41 | [30 (enc/msecs :hours 6)] ; Max 30 posts in 6 hours
42 | ]
43 |
44 | Options:
45 | `:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
46 | `:conn-opts` - Map of connection opts given to `clj-slack.chat/post-message`
47 | Examples:
48 | {:token \"MY-TOKEN\"}
49 | {:token \"MY-TOKEN\", :api-url \"https://slack.com/api\"}
50 |
51 | `:post-opts` - Map of post opts given to `clj-slack.chat/post-message`
52 | Examples:
53 | {:channel-id \"C12345678\", :username \"MY_BOT\"}
54 |
55 | Tips:
56 | - See `clj-slack` docs for more info on its options."
57 |
58 | ;; ([] (handler:slack nil))
59 | ([{:keys [conn-opts post-opts output-fn]
60 | :or
61 | {conn-opts {:api-url "https://slack.com/api", :token nil}
62 | post-opts {:channel-id nil, :username nil}
63 | output-fn (utils/format-signal-fn)}}]
64 |
65 | (let [{:keys [api-url token]
66 | :or {api-url "https://slack.com/api"}} conn-opts
67 |
68 | {:keys [channel-id]} post-opts
69 | post-opts (dissoc post-opts :channel-id)
70 |
71 | _ (when-not (string? token) (truss/ex-info! "Expected `:conn-opts/token` string" (truss/typed-val token)))
72 | _ (when-not (string? channel-id) (truss/ex-info! "Expected `:post-opts/channel-id` string" (truss/typed-val channel-id)))
73 |
74 | handler-fn
75 | (fn a-handler:slack
76 | ([ ]) ; Stop => noop
77 | ([signal]
78 | (when-let [output (output-fn signal)]
79 | (slack.chat/post-message conn-opts channel-id
80 | output post-opts))))]
81 |
82 | (with-meta handler-fn
83 | {:dispatch-opts default-dispatch-opts}))))
84 |
--------------------------------------------------------------------------------
/main/src/taoensso/telemere/tools_logging.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.telemere.tools-logging
2 | "tools.logging -> Telemere interop.
3 | Telemere will attempt to load this ns automatically when possible.
4 |
5 | Naming conventions:
6 | `tools.logging` - For referring to the library.
7 | `tools-logging` - For symbols, keywords, and this namespace.
8 | `clojure.tools.logging` - For env config to match library's conventions."
9 |
10 | (:require
11 | [taoensso.truss :as truss]
12 | [taoensso.encore :as enc]
13 | [taoensso.telemere.impl :as impl]
14 | [clojure.tools.logging :as ctl]))
15 |
16 | (defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))
17 |
18 | (deftype TelemereLogger [logger-name]
19 | ;; `logger-name` is typically ns string
20 | clojure.tools.logging.impl/Logger
21 | (enabled? [_ level]
22 | (when-debug (println [:tools-logging/enabled? level logger-name]))
23 | (impl/signal-allowed?
24 | {:ns logger-name
25 | :kind :tools-logging
26 | :level level}))
27 |
28 | (write! [_ level throwable message]
29 | (when-debug (println [:tools-logging/write! level logger-name]))
30 | (impl/signal!
31 | {:allow? true ; Pre-filtered by `enabled?` call
32 | :ns logger-name
33 | :kind :tools-logging
34 | :level level
35 | :error throwable
36 | :msg message})
37 | nil))
38 |
39 | (deftype TelemereLoggerFactory []
40 | clojure.tools.logging.impl/LoggerFactory
41 | (name [_ ] "taoensso.telemere")
42 | (get-logger [_ logger-name] (TelemereLogger. (str logger-name))))
43 |
44 | (defn tools-logging->telemere!
45 | "Configures tools.logging to use Telemere as its logging
46 | implementation (backend).
47 |
48 | Called automatically if one of the following is \"true\":
49 | 1. JVM property: `clojure.tools.logging.to-telemere`
50 | 2. Env variable: `CLOJURE_TOOLS_LOGGING_TO_TELEMERE`
51 | 3. Classpath resource: `clojure.tools.logging.to-telemere`"
52 | []
53 | (impl/signal!
54 | {:kind :event
55 | :level :debug ; < :info since runs on init
56 | :id :taoensso.telemere/tools-logging->telemere!
57 | :msg "Enabling interop: tools.logging -> Telemere"})
58 |
59 | (alter-var-root #'clojure.tools.logging/*logger-factory*
60 | (fn [_] (TelemereLoggerFactory.))))
61 |
62 | (defn tools-logging->telemere?
63 | "Returns true iff tools.logging is configured to use Telemere
64 | as its logging implementation (backend)."
65 | []
66 | (when-let [lf clojure.tools.logging/*logger-factory*]
67 | (instance? TelemereLoggerFactory lf)))
68 |
69 | ;;;;
70 |
71 | (defn check-interop
72 | "Returns interop debug info map."
73 | []
74 | (let [sending? (tools-logging->telemere?)
75 | receiving?
76 | (and sending?
77 | (impl/test-interop! "tools.logging -> Telemere"
78 | #(clojure.tools.logging/info %)))]
79 |
80 | {:present? true
81 | :enabled-by-env? impl/enabled:tools-logging?
82 | :sending->telemere? sending?
83 | :telemere-receiving? receiving?}))
84 |
85 | (impl/add-interop-check! :tools-logging check-interop)
86 |
87 | (impl/on-init
88 | (when impl/enabled:tools-logging?
89 | (tools-logging->telemere!)))
90 |
--------------------------------------------------------------------------------
/main/resources/docs/signal-options.txt:
--------------------------------------------------------------------------------
1 | Signal options are provided as a map with COMPILE-TIME keys.
2 | All options are available for all signal creator calls:
3 |
4 | `:inst` -------- Platform instant [1] when signal was created, ∈ #{nil :auto <[1]>}
5 | `:level` ------- Signal level ∈ #{ :trace :debug :info :warn :error :fatal :report ...}
6 | `:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy ...}
7 | `:id` ---------- ?id of signal (common to all signals created at callsite, contrast with `:uid`)
8 | `:uid` --------- ?id of signal instance (unique to each signal created at callsite, contrast with `:id`)
9 | Defaults to `:auto` for tracing signals, and nil otherwise
10 |
11 | `:msg` --------- Arb app-level ?message to incl. in signal: str or vec of strs to join (with `\space`), may be a delay
12 | `:data` -------- Arb app-level ?data to incl. in signal: usu. a map, LAZY! [3]
13 | `:error` ------- Arb app-level ?error to incl. in signal: platform error [2]
14 |
15 | `:run` --------- ?form to execute UNCONDITIONALLY; will incl. `:run-val` in signal
16 | `:do` ---------- ?form to execute conditionally (iff signal allowed) and LAZILY [3], before establishing `:let` ?binding
17 | `:let` --------- ?bindings to establish conditionally (iff signal allowed) and LAZILY [3], BEFORE evaluating `:data` and `:msg` (useful!)
18 |
19 | `:parent` ------ Custom ?{:keys [id uid]} to override auto (dynamic) parent signal tracing info
20 | `:root` -------- Custom ?{:keys [id uid]} to override auto (dynamic) root signal tracing info
21 | `:ctx` --------- Custom ?val to override auto (dynamic `*ctx*`) in signal, as per `with-ctx`
22 | `:ctx+` -------- Custom ?val to update auto (dynamic `*ctx*`) in signal, as per `with-ctx+`
23 |
24 | `:ns` ---------- Custom ?str namespace to override auto signal callsite info
25 | `:coords` ------ Custom ?[line column] to override auto signal callsite info
26 |
27 | `:elidable?` --- Should signal be subject to compile-time elision? (default true)
28 | `:allow?` ------ Custom override for usual runtime filtering (true => ALWAYS create signal)
29 | `:trace?` ------ Should tracing be enabled for `:run` form?
30 |
31 | `:sample` ------ Sample ?rate ∈ℝ[0,1] for random signal sampling (0.75 => allow 75% of signals, nil => allow all)
32 | `:when` -------- Arb ?form; when present, form must return truthy to allow signal
33 | `:limit` ------- Rate limit ?spec given to `taoensso.telemere/rate-limiter`, see its docstring for details
34 | `:limit-by` ---- When present, rate limits will be enforced independently for each value (any Clojure value!)
35 | `:xfn` --------- Optional transform (fn [signal]) => ?modified-signal to apply when signal is created, as per `with-xfn`
36 | `:xfn+` -------- Optional extra transform (fn [signal]) => ?modified-signal to apply when signal is created, as per `with-xfn+`
37 |
38 | ---------- Other arb app-level ?kvs to incl. in signal. Typically NOT included in
39 | handler output, so a great way to provide custom data/opts for use
40 | (only) by custom transforms/handlers. LAZY! [3]
41 |
42 | If anything is unclear, please ping me (@ptaoussanis) so that I can improve these docs!
43 |
44 | [1] `java.time.Instant` or `js/Date`
45 | [2] `java.lang.Throwable` or `js/Error`
46 | [3] Most Telemere signal content is evaluated CONDITIONALLY (iff signal allowed),
47 | LAZILY (when signal is created), and on the HANDLING THREAD (not logging thread).
48 | This allows efficient filtering, better control+monitoring of back pressure,
49 | conditional effects, etc. Ref. for visual!
50 |
--------------------------------------------------------------------------------
/main/project.clj:
--------------------------------------------------------------------------------
1 | (defproject com.taoensso/telemere "1.2.1"
2 | :author "Peter Taoussanis "
3 | :description "Structured logs and telemetry for Clojure/Script"
4 | :url "https://www.taoensso.com/telemere"
5 |
6 | :license
7 | {:name "Eclipse Public License - v 1.0"
8 | :url "https://www.eclipse.org/legal/epl-v10.html"}
9 |
10 | :scm {:name "git" :url "https://github.com/taoensso/telemere"}
11 |
12 | :dependencies
13 | [[com.taoensso/encore "3.159.0"]]
14 |
15 | :test-paths ["test" #_"src"]
16 |
17 | :profiles
18 | {;; :default [:base :system :user :provided :dev]
19 | :provided {:dependencies [[org.clojure/clojurescript "1.12.134"]
20 | [org.clojure/clojure "1.11.4"]]}
21 | :c1.12 {:dependencies [[org.clojure/clojure "1.12.3"]]}
22 | :c1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]}
23 | :c1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]}
24 |
25 | :graal-tests
26 | {:source-paths ["test"]
27 | :main taoensso.graal-tests
28 | :aot [taoensso.graal-tests]
29 | :uberjar-name "graal-tests.jar"
30 | :dependencies
31 | [[org.clojure/clojure "1.11.4"]
32 | [com.github.clj-easy/graal-build-time "1.0.5"]]}
33 |
34 | :test {:aot [] #_[taoensso.telemere-tests]}
35 | :ott-on {:jvm-opts ["-Dtaoensso.telemere.otel-tracing=true"]}
36 | :ott-off {:jvm-opts ["-Dtaoensso.telemere.otel-tracing=false"]}
37 | :dev
38 | {:jvm-opts
39 | ["-server"
40 | "-Dtaoensso.elide-deprecated=true"
41 | "-Dclojure.tools.logging.to-telemere=true"]
42 |
43 | :global-vars
44 | {*warn-on-reflection* true
45 | *assert* true
46 | *unchecked-math* false #_:warn-on-boxed}
47 |
48 | :dependencies
49 | [[org.clojure/core.async "1.8.741"]
50 | [org.clojure/test.check "1.1.2"]
51 | [org.clojure/tools.logging "1.3.0"]
52 | [org.slf4j/slf4j-api "2.0.17"]
53 | [com.taoensso/telemere-slf4j "1.2.1"]
54 | #_[org.slf4j/slf4j-simple "2.0.16"]
55 | #_[org.slf4j/slf4j-nop "2.0.16"]
56 | #_[io.github.paintparty/bling "0.4.2"]
57 |
58 | ;;; For optional handlers
59 | [io.opentelemetry/opentelemetry-api "1.57.0"]
60 | [io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.57.0"]
61 | [io.opentelemetry/opentelemetry-exporter-otlp "1.57.0"]
62 | #_[io.opentelemetry/opentelemetry-exporters-jaeger "0.9.1"]
63 | [metosin/jsonista "0.3.13"]
64 | [com.draines/postal "2.0.5"]
65 | [org.julienxx/clj-slack "0.8.3"]]
66 |
67 | :plugins
68 | [[lein-pprint "1.3.2"]
69 | [lein-ancient "0.7.0"]
70 | [lein-cljsbuild "1.1.8"]]}}
71 |
72 | :cljsbuild
73 | {:test-commands {"node" ["node" "target/test.js"]}
74 | :builds
75 | [{:id :main
76 | :source-paths ["src"]
77 | :compiler
78 | {:output-to "target/main.js"
79 | :optimizations :advanced}}
80 |
81 | {:id :test
82 | :source-paths ["src" "test"]
83 | :compiler
84 | {:output-to "target/test.js"
85 | :target :nodejs
86 | :optimizations :simple}}]}
87 |
88 | :aliases
89 | {"start-dev" ["with-profile" "+dev" "repl" ":headless"]
90 | "build-once" ["do" ["clean"] ["cljsbuild" "once"]]
91 | "deploy-lib" ["do" ["build-once"] ["deploy" "clojars"] ["install"]]
92 |
93 | "test-clj" ["with-profile" "+c1.12:+c1.11:+c1.10" "test"]
94 | "test-cljs" ["with-profile" "+c1.12" "cljsbuild" "test"]
95 |
96 | "test-clj-ott-off" ["with-profile" "+ott-off" "test-clj"]
97 | "test-all" ["do" ["clean"] ["test-clj"] ["test-clj-ott-off"] ["test-cljs"]]})
98 |
--------------------------------------------------------------------------------
/slf4j/src/java/com/taoensso/telemere/slf4j/TelemereLogger.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2004-2011 QOS.ch
3 | * All rights reserved.
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining
6 | * a copy of this software and associated documentation files (the
7 | * "Software"), to deal in the Software without restriction, including
8 | * without limitation the rights to use, copy, modify, merge, publish,
9 | * distribute, sublicense, and/or sell copies of the Software, and to
10 | * permit persons to whom the Software is furnished to do so, subject to
11 | * the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be
14 | * included in all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | *
24 | */
25 | package com.taoensso.telemere.slf4j;
26 | // Based on `org.slf4j.simple.SimpleLogger`
27 |
28 | import java.io.Serializable;
29 |
30 | import org.slf4j.Logger;
31 | import org.slf4j.Marker;
32 | import org.slf4j.event.Level;
33 | import org.slf4j.event.LoggingEvent;
34 | import org.slf4j.helpers.LegacyAbstractLogger;
35 | import org.slf4j.spi.LoggingEventAware;
36 |
37 | import clojure.java.api.Clojure;
38 | import clojure.lang.IFn;
39 |
40 | public class TelemereLogger extends LegacyAbstractLogger implements LoggingEventAware, Serializable {
41 |
42 | private static final long serialVersionUID = -1999356203037132557L;
43 |
44 | private static boolean INITIALIZED = false;
45 | static void lazyInit() {
46 | if (INITIALIZED) { return; }
47 | INITIALIZED = true;
48 | init();
49 | }
50 |
51 | private static IFn logFn;
52 | private static IFn isAllowedFn;
53 |
54 | static void init() {
55 | IFn requireFn = Clojure.var("clojure.core", "require");
56 | requireFn.invoke(Clojure.read("taoensso.telemere.slf4j"));
57 | isAllowedFn = Clojure.var("taoensso.telemere.slf4j", "allowed?");
58 | logFn = Clojure.var("taoensso.telemere.slf4j", "log!");
59 | }
60 |
61 | protected TelemereLogger(String name) { this.name = name; }
62 |
63 | protected boolean isLevelEnabled(Level level) { return (boolean) isAllowedFn.invoke(this.name, level); }
64 | public boolean isTraceEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.TRACE); }
65 | public boolean isDebugEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.DEBUG); }
66 | public boolean isInfoEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.INFO); }
67 | public boolean isWarnEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.WARN); }
68 | public boolean isErrorEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.ERROR); }
69 |
70 | public void log(LoggingEvent event) { logFn.invoke(this.name, event); } // Fluent (modern) API, called after level check
71 |
72 | @Override protected String getFullyQualifiedCallerName() { return null; }
73 | @Override
74 | protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) {
75 | logFn.invoke(this.name, level, throwable, messagePattern, arguments, marker); // Legacy API, called after level check
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/main/resources/docs/spy!.txt:
--------------------------------------------------------------------------------
1 | "Spy" signal creator, emphasizing (optional level) + form to run.
2 |
3 | Default kind: `:spy`
4 | Default level: `:info`
5 | Returns: ALWAYS (unconditionally) returns run value, or rethrows run error.
6 |
7 | When filtering conditions are met [4], creates a Telemere signal [3] and
8 | dispatches it to registered handlers for processing (e.g. writing to
9 | console/file/queue/db, etc.).
10 |
11 | Enables tracing of given `run` form:
12 |
13 | - Resulting signal will include {:keys [run-form run-val run-nsecs]}.
14 | - Nested signals will include this signal's id and uid under `:parent`.
15 |
16 | Limitations:
17 |
18 | 1. Traced `run` form is usually expected to be synchronous and eager.
19 | So no lazy seqs, async calls, or inversion of flow control (IoC) macros like
20 | core.async `go` blocks, etc.
21 |
22 | 2. Tracing call (`spy!`) is usually expected to occur *within* normally flowing code.
23 | IoC macros can arbitrarily (and often opaquely) alter program flow and tracing
24 | across flow boundaries can be fragile or even fundamentally illogical.
25 |
26 | So use within IoC macro bodies might not make conceptual sense, or could produce
27 | errors or unreliable/confusing results.
28 |
29 | Basically- if possible, prefer tracing normal Clojure fns running within normal
30 | Clojure fns unless you deeply understand what your IoC macros are up to.
31 |
32 | Examples:
33 |
34 | (spy! (+ 1 2)) ; %> {:kind :trace, :level :info, :run-form '(+ 1 2),
35 | ; :run-val 3, :run-nsecs , :parent {:keys [id uid]}
36 | ; :msg "(+ 1 2) => 3" ...}
37 | (spy! :debug (+ 1 2)) ; %> {... :level :debug ...}
38 | (spy!
39 | {:let [x "x"] ; Available to `:data` and `:msg`
40 | :data {:x x}
41 | :msg ["My message:" x]}
42 |
43 | (+ 1 2)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
44 |
45 | Tips:
46 |
47 | - Test using `with-signal`: (with-signal (spy! ...)).
48 | - Supports the same options [2] as other signals [1].
49 |
50 | - Like `trace!`, but takes optional level rather than optional id.
51 |
52 | - Useful for debugging/monitoring forms, and tracing (nested) execution flow.
53 | - Execution of `run` form may create additional (nested) signals.
54 | Each signal's `:parent` key will indicate its immediate parent.
55 |
56 | - It's often useful to wrap `run` form with `catch->error!`:
57 | (trace! ::trace-id (catch->error! ::error-id ...)).
58 |
59 | This way you have independent filtering for `run` forms that throw,
60 | allowing you to use a higher min level and/or reduced sampling, etc.
61 |
62 | In this case you'll create:
63 | 0 or 1 `:trace` signals (depending on filtering), AND
64 | 0 or 1 `:error` signals (depending on filtering).
65 |
66 | Note that the `:error` signal will contain tracing info (e.g. `:parent` key)
67 | iff the enclosing `trace!` is allowed.
68 |
69 | - Runtime of async or lazy code in `run` form will intentionally NOT be
70 | included in resulting signal's `:run-nsecs` value. If you want to measure
71 | such runtimes, make sure that your form wraps where the relevant costs are
72 | actually realized. Compare:
73 | (spy! (delay (my-slow-code))) ; Doesn't measure slow code
74 | (spy! @(delay (my-slow-code))) ; Does measure slow code
75 |
76 | - See also Tufte (https://www.taoensso.com/tufte) for a complementary/partner
77 | Clj/s library that offers more advanced performance measurment and shares
78 | the same signal engine (filtering and handler API) as Telemere.
79 |
80 | ----------------------------------------------------------------------
81 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
82 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
83 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
84 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
85 |
--------------------------------------------------------------------------------
/main/resources/docs/trace!.txt:
--------------------------------------------------------------------------------
1 | "Trace" signal creator, emphasizing (optional id) + form to run.
2 |
3 | Default kind: `:trace`
4 | Default level: `:info` (intentionally NOT `:trace`!)
5 | Returns: ALWAYS (unconditionally) returns run value, or rethrows run error.
6 |
7 | When filtering conditions are met [4], creates a Telemere signal [3] and
8 | dispatches it to registered handlers for processing (e.g. writing to
9 | console/file/queue/db, etc.).
10 |
11 | Enables tracing of given `run` form:
12 |
13 | - Resulting signal will include {:keys [run-form run-val run-nsecs]}.
14 | - Nested signals will include this signal's id and uid under `:parent`.
15 |
16 | Limitations:
17 |
18 | 1. Traced `run` form is usually expected to be synchronous and eager.
19 | So no lazy seqs, async calls, or inversion of flow control (IoC) macros like
20 | core.async `go` blocks, etc.
21 |
22 | 2. Tracing call (`trace!`) is usually expected to occur *within* normally flowing code.
23 | IoC macros can arbitrarily (and often opaquely) alter program flow and tracing
24 | across flow boundaries can be fragile or even fundamentally illogical.
25 |
26 | So use within IoC macro bodies might not make conceptual sense, or could produce
27 | errors or unreliable/confusing results.
28 |
29 | Basically- if possible, prefer tracing normal Clojure fns running within normal
30 | Clojure fns unless you deeply understand what your IoC macros are up to.
31 |
32 | Examples:
33 |
34 | (trace! (+ 1 2)) ; %> {:kind :trace, :level :info, :run-form '(+ 1 2),
35 | ; :run-val 3, :run-nsecs , :parent {:keys [id uid]} ...
36 | ; :msg "(+ 1 2) => 3" ...}
37 | (trace! ::my-id (+ 1 2)) ; %> {... :id ::my-id ...}
38 | (trace!
39 | {:let [x "x"] ; Available to `:data` and `:msg`
40 | :data {:x x}
41 | :msg ["My message:" x]}
42 |
43 | (+ 1 2)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
44 |
45 | Tips:
46 |
47 | - Test using `with-signal`: (with-signal (trace! ...)).
48 | - Supports the same options [2] as other signals [1].
49 |
50 | - Like `spy!`, but takes optional id rather than optional level.
51 |
52 | - Useful for debugging/monitoring forms, and tracing (nested) execution flow.
53 | - Execution of `run` form may create additional (nested) signals.
54 | Each signal's `:parent` key will indicate its immediate parent.
55 |
56 | - It's often useful to wrap `run` form with `catch->error!`:
57 | (trace! ::trace-id (catch->error! ::error-id ...)).
58 |
59 | This way you have independent filtering for `run` forms that throw,
60 | allowing you to use a higher min level and/or reduced sampling, etc.
61 |
62 | In this case you'll create:
63 | 0 or 1 `:trace` signals (depending on filtering), AND
64 | 0 or 1 `:error` signals (depending on filtering).
65 |
66 | Note that the `:error` signal will contain tracing info (e.g. `:parent` key)
67 | iff the enclosing `trace!` is allowed.
68 |
69 | - Default level is `:info`, not `:trace`! The name "trace" in "trace signal"
70 | refers to the general action of tracing program flow rather than to the
71 | common logging level of the same name.
72 |
73 | - Runtime of async or lazy code in `run` form will intentionally NOT be
74 | included in resulting signal's `:run-nsecs` value. If you want to measure
75 | such runtimes, make sure that your form wraps where the relevant costs are
76 | actually realized. Compare:
77 | (trace! (delay (my-slow-code))) ; Doesn't measure slow code
78 | (trace! @(delay (my-slow-code))) ; Does measure slow code
79 |
80 | - See also Tufte (https://www.taoensso.com/tufte) for a complementary/partner
81 | Clj/s library that offers more advanced performance measurment and shares
82 | the same signal engine (filtering and handler API) as Telemere.
83 |
84 | ----------------------------------------------------------------------
85 | [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
86 | [2] See `help:signal-options` - {:keys [kind level id data ...]}
87 | [3] See `help:signal-content` - {:keys [kind level id data ...]}
88 | [4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
89 |
--------------------------------------------------------------------------------
/wiki/5-Migrating.md:
--------------------------------------------------------------------------------
1 | # From Timbre
2 |
3 | While [Timbre](https://taoensso.com/timbre) will **continue to be maintained and supported** (and will even receive some improvements back-ported from Telemere), most Timbre users will want to at least *consider* updating to Telemere.
4 |
5 | Telemere's functionality is a **superset of Timbre**, and offers *many* improvements including:
6 |
7 | - Better support for [structured logging](./1-Getting-started#data-types-and-structures)
8 | - Better [performance](https://github.com/taoensso/telemere#performance)
9 | - Better [documentation](https://github.com/taoensso/telemere#documentation)
10 | - A more flexible [API](./1-Getting-started#usage) that unifies all telemetry and logging needs
11 | - A more robust [architecture](./2-Architecture), free from all historical constraints
12 | - Better [included handlers](./4-Handlers##included-handlers)
13 | - Easier [configuration](./3-Config)
14 |
15 | Migrating from Timbre to Telemere should be straightforward **unless you depend on specific/custom appenders** that might not be available for Telemere (yet).
16 |
17 | ## Checklist
18 |
19 | ### 1. Appenders
20 |
21 | Where Timbre uses the term "appender", Telemere uses the more general "handler". Functionally they're the same thing.
22 |
23 | Check which **Timbre appenders** you use, and whether a similar handler is [currently included](./4-Handlers#included-handlers) with Telemere or available via the [community](./8-Community#handlers-and-tools).
24 |
25 | If not, you may need to [write something yourself](./4-Handlers#writing-handlers).
26 |
27 | This may be easier than it sounds. Remember that signals are just plain Clojure/Script [maps](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content), and handlers just plain Clojure/Script functions that do something with those maps.
28 |
29 | Feel free to [ping me](https://github.com/taoensso/telemere/issues) for assistance, or ask on the [`#telemere` Slack channel](https://www.taoensso.com/telemere/slack).
30 |
31 | ### 2. Logging calls
32 |
33 | What about all the Timbre logging calls in your code?
34 |
35 | You've got two choices-
36 |
37 | #### 2a. Redirect Timbre output to Telemere
38 |
39 | Add [`taoensso.telemere.timbre/timbre->telemere-appender`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.timbre#timbre->telemere-appender) as a Timbre appender. It'll redirect Timbre's output to Telemere.
40 |
41 | In this case you may want to disable all your other Timbre appenders, and all your Timbre filtering.
42 |
43 | #### 2b. Change your ns imports
44 |
45 | The [`taoensso.telemere.timbre`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.timbre) namespace contains a shim of most of Timbre's API so you can switch your Timbre namespace imports:
46 |
47 | ```clojure
48 | (ns my-ns
49 | (:require [taoensso.timbre :as timbre :refer [...]]) ; Old
50 | (:require [taoensso.telemere.timbre :as timbre :refer [...]]) ; New
51 | )
52 | ```
53 |
54 | In this case your Timbre appenders and filtering will be ignored.
55 |
56 | Feel free to keep using the shim API **as long as you like**, there's no need to rewrite any of your existing code unless you specifically want to use features that are only possible with Telemere's [signal creators](./1-Getting-started#create-signals), etc.
57 |
58 | ### 3. Config
59 |
60 | You *may* need to update code related to filter config and/or handler management.
61 |
62 | This is usually only a few lines of code, and *should* be straightforward.
63 |
64 | See section [3-Config](./3-Config) for more info on configuring Telemere.
65 |
66 | ### 4. Testing
67 |
68 | While I believe that the Timbre shim above *should* be robust, it's of course possible that I missed something.
69 |
70 | So **please test carefully** before switching to Telemere in production, and **please [report](https://github.com/taoensso/telemere/issues) any issues**! 🙏
71 |
72 | In particular - note that Telemere's **handler output** may be **completely different**, so if you have any code/systems (e.g. log aggregators) that depend on the specific output format - **these must also be tested**.
73 |
74 | If for any reason your tests are unsuccessful, please don't feel pressured to migrate. Again, I will **continue to maintain and support Timbre**. I have applications running Timbre that I plan to **never migrate** since they're completely stable.
75 |
76 | # From tools.logging
77 |
78 | This is easy, see [here](./3-Config#toolslogging).
79 |
80 | # From Java logging
81 |
82 | This is easy, see [here](./3-Config#java-logging).
--------------------------------------------------------------------------------
/main/src/taoensso/telemere/postal.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.telemere.postal
2 | "Telemere -> email handler using `postal`,
3 | Ref. ."
4 | (:require
5 | [taoensso.truss :as truss]
6 | [taoensso.encore :as enc]
7 | [taoensso.encore.signals :as sigs]
8 | [taoensso.telemere.utils :as utils]
9 | [postal.core :as postal]))
10 |
11 | (comment
12 | (require '[taoensso.telemere :as tel])
13 | (remove-ns (symbol (str *ns*)))
14 | (:api (enc/interns-overview)))
15 |
16 | (def default-dispatch-opts
17 | {:min-level :info
18 | :limit
19 | [[5 (enc/msecs :mins 1)]
20 | [10 (enc/msecs :mins 15)]
21 | [15 (enc/msecs :hours 1)]
22 | [30 (enc/msecs :hours 6)]]})
23 |
24 | (defn handler:postal
25 | "Alpha, subject to change.
26 |
27 | Needs `postal`, Ref. .
28 |
29 | Returns a signal handler that:
30 | - Takes a Telemere signal (map).
31 | - Sends the signal as an email to specified recipient.
32 |
33 | Useful for emailing important alerts to admins, etc.
34 |
35 | Default handler dispatch options (override when calling `add-handler!`):
36 | `:min-level` - `:info`
37 | `:limit` -
38 | [[5 (enc/msecs :mins 1)] ; Max 5 emails in 1 min
39 | [10 (enc/msecs :mins 15)] ; Max 10 emails in 15 mins
40 | [15 (enc/msecs :hours 1)] ; Max 15 emails in 1 hour
41 | [30 (enc/msecs :hours 6)] ; Max 30 emails in 6 hours
42 | ]
43 |
44 | Options:
45 | `:conn-opts` - Map of connection opts given to `postal/send-message`
46 | Examples:
47 | {:host \"mail.isp.net\", :user \"jsmith\", :pass \"a-secret\"},
48 | {:host \"smtp.gmail.com\", :user \"jsmith@gmail.com\", :pass \"a-secret\" :port 587 :tls true},
49 | {:host \"email-smtp.us-east-1.amazonaws.com\", :port 587, :tls true,
50 | :user \"AKIAIDTP........\", :pass \"AikCFhx1P.......\"}
51 |
52 | `:msg-opts` - Map of message opts given to `postal/send-message`
53 | Examples:
54 | {:from \"foo@example.com\", :to \"bar@example.com\"},
55 | {:from \"Alice \"},
56 | {:from \"no-reply@example.com\", :to [\"first-responders@example.com\",
57 | \"devops@example.com\"],
58 | :cc \"engineering@example.com\"
59 | :X-MyHeader \"A custom header\"}
60 |
61 | `:subject-fn` ------ (fn [signal]) => email subject string
62 | `:subject-max-len` - Truncate subjects beyond this length (default 90)
63 |
64 | `:body-fn` - (fn [signal]) => email body content string,
65 | see `format-signal-fn` or `pr-signal-fn`
66 |
67 | Tips:
68 | - Ref. for more info on `postal` options.
69 | - Sending emails can be slow, and can incur financial costs!
70 | Use appropriate handler dispatch options for async handling and rate limiting, etc."
71 |
72 | ;; ([] (handler:postal nil))
73 | ([{:keys [conn-opts msg-opts, subject-fn subject-max-len body-fn]
74 | :or
75 | {body-fn (utils/format-signal-fn)
76 | subject-fn (utils/signal-preamble-fn {:format-inst-fn nil})
77 | subject-max-len 128}}]
78 |
79 | (when-not (map? conn-opts) (truss/ex-info! "Expected `:conn-opts` map" (truss/typed-val conn-opts)))
80 | (when-not (map? msg-opts) (truss/ex-info! "Expected `:msg-opts` map" (truss/typed-val msg-opts)))
81 |
82 | (let [subject-fn
83 | (if-let [n subject-max-len]
84 | (comp
85 | (fn [s] (when s (enc/substr (str s) 0 n)))
86 | subject-fn))
87 |
88 | handler-fn
89 | (fn a-handler:postal
90 | ([ ]) ; Stop => noop
91 | ([signal]
92 | (enc/when-let [subject (subject-fn signal)
93 | body (body-fn signal)]
94 | (let [msg
95 | (assoc msg-opts
96 | :subject (str subject)
97 | :body
98 | (if (string? body)
99 | [{:type "text/plain; charset=utf-8"
100 | :content (str body)}]
101 | body))
102 |
103 | [result ex]
104 | (try
105 | [(postal/send-message conn-opts msg) nil]
106 | (catch Exception ex [nil ex]))
107 |
108 | success? (= (get result :code) 0)]
109 |
110 | (when-not success?
111 | (truss/ex-info! "Failed to send email" result ex))))))]
112 |
113 | (with-meta handler-fn
114 | {:dispatch-opts default-dispatch-opts}))))
115 |
--------------------------------------------------------------------------------
/main/src/taoensso/telemere/sockets.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.telemere.sockets
2 | "Telemere -> TCP/UDP socket handlers."
3 | (:require
4 | [taoensso.truss :as truss]
5 | [taoensso.encore :as enc]
6 | [taoensso.telemere.utils :as utils])
7 |
8 | (:import
9 | [java.net Socket InetAddress]
10 | [java.net DatagramSocket DatagramPacket InetSocketAddress]
11 | [java.io PrintWriter]))
12 |
13 | (comment
14 | (require '[taoensso.telemere :as tel])
15 | (remove-ns (symbol (str *ns*)))
16 | (:api (enc/interns-overview)))
17 |
18 | (defn handler:tcp-socket
19 | "Experimental, subject to change.
20 |
21 | Returns a signal handler that:
22 | - Takes a Telemere signal (map).
23 | - Sends the signal as a string to specified TCP socket.
24 |
25 | Can output signals as human or machine-readable (edn, JSON) strings.
26 |
27 | Options:
28 | `:output-fn` --- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
29 | `:socket-opts` - {:keys [host port ssl? connect-timeout-msecs]}
30 | `:host` ------ Destination TCP socket hostname string
31 | `:port` ------ Destination TCP socket port int
32 | `:ssl?` ------ Use SSL/TLS (default false)
33 | `:connect-timeout-msecs` - Connection timeout (default 3000 msecs)
34 |
35 | Limitations:
36 | - Failed writes will be retried only once.
37 | - Writes lock on a single underlying socket, so IO won't benefit from adding
38 | extra handler threads. Let me know if there's demand for socket pooling."
39 |
40 | ;; ([] (handler:tcp-socket nil))
41 | ([{:keys [socket-opts output-fn]
42 | :or {output-fn (utils/format-signal-fn)}}]
43 |
44 | (let [sw (utils/tcp-socket-writer socket-opts)]
45 | (fn a-handler:tcp-socket
46 | ([ ] (sw)) ; Stop => close socket
47 | ([signal]
48 | (when-let [output (output-fn signal)]
49 | (sw output)))))))
50 |
51 | (defn handler:udp-socket
52 | "Highly experimental, subject to change!
53 | Feedback very welcome!
54 |
55 | Returns a signal handler that:
56 | - Takes a Telemere signal (map).
57 | - Sends the signal as a string to specified UDP socket.
58 |
59 | Can output signals as human or machine-readable (edn, JSON) strings.
60 |
61 | Options:
62 | `:output-fn` ---------- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
63 | `:socket-opts` -------- {:keys [host port max-packet-bytes]}
64 | `:host` ------------- Destination UDP socket hostname string
65 | `:port` ------------- Destination UDP socket port int
66 | `:max-packet-bytes` - Max packet size (in bytes) before truncating output (default 512)
67 |
68 | `:truncation-warning-fn`
69 | Optional (fn [{:keys [max actual signal]}]) to call whenever output is truncated.
70 | Should be appropriately rate-limited!
71 |
72 | Limitations:
73 | - Due to UDP limitations, truncates output to `max-packet-bytes`!
74 | - Failed writes will be retried only once.
75 | - Writes lock on a single underlying socket, so IO won't benefit from adding
76 | extra handler threads. Let me know if there's demand for socket pooling.
77 | - No DTLS (Datagram Transport Layer Security) support,
78 | please let me know if there's demand."
79 |
80 | ;; ([] (handler:udp-socket nil))
81 | ([{:keys [socket-opts output-fn truncation-warning-fn]
82 | :or
83 | {socket-opts {:max-packet-bytes 512}
84 | output-fn (utils/format-signal-fn)}}]
85 |
86 | (let [{:keys [host port max-packet-bytes]
87 | :or {max-packet-bytes 512}} socket-opts
88 |
89 | max-packet-bytes (int max-packet-bytes)
90 |
91 | socket (DatagramSocket.) ; No need to change socket once created
92 | lock (Object.)]
93 |
94 | (when-not (string? host) (truss/ex-info! "Expected `:host` string" (truss/typed-val host)))
95 | (when-not (int? port) (truss/ex-info! "Expected `:port` int" (truss/typed-val port)))
96 |
97 | (.connect socket (InetSocketAddress. (str host) (int port)))
98 |
99 | (fn a-handler:udp-socket
100 | ([ ] (locking lock (.close socket))) ; Stop => close socket
101 | ([signal]
102 | (when-let [output (output-fn signal)]
103 | (let [ba (enc/str->utf8-ba (str output))
104 | ba-len (alength ba)
105 | packet (DatagramPacket. ba (min ba-len max-packet-bytes))]
106 |
107 | (when (and truncation-warning-fn (> ba-len max-packet-bytes))
108 | ;; Fn should be appropriately rate-limited
109 | (truncation-warning-fn {:max max-packet-bytes, :actual ba-len, :signal signal}))
110 |
111 | (locking lock
112 | (try
113 | (.send (DatagramSocket.) packet)
114 | (catch Exception _ ; Retry once
115 | (Thread/sleep 250)
116 | (.send (DatagramSocket.) packet)))))))))))
117 |
--------------------------------------------------------------------------------
/main/src/taoensso/telemere/consoles.cljc:
--------------------------------------------------------------------------------
1 | (ns ^:no-doc taoensso.telemere.consoles
2 | "Telemere -> console handlers."
3 | (:require
4 | [taoensso.truss :as truss]
5 | [taoensso.encore :as enc]
6 | [taoensso.telemere.utils :as utils]))
7 |
8 | (comment
9 | (require '[taoensso.telemere :as tel])
10 | (remove-ns (symbol (str *ns*)))
11 | (:api (enc/interns-overview)))
12 |
13 | #?(:clj
14 | (defn ^:public handler:console
15 | "Alpha, subject to change.
16 | Returns a signal handler that:
17 | - Takes a Telemere signal (map).
18 | - Writes the signal as a string to specified stream.
19 |
20 | A general-purpose `println`-style handler that's well suited for outputting
21 | signals as human or machine-readable (edn, JSON) strings.
22 |
23 | Options:
24 | `:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
25 | `:stream` ---- `java.io.writer`
26 | Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise."
27 |
28 | ([] (handler:console nil))
29 | ([{:keys [stream output-fn]
30 | :or
31 | {stream :auto
32 | output-fn (utils/format-signal-fn)}}]
33 |
34 | (let [error-signal? utils/error-signal?]
35 |
36 | (fn a-handler:console
37 | ([ ]) ; Stop => noop
38 | ([signal]
39 | (let [^java.io.Writer stream
40 | (case stream
41 | (:out :*out*) *out*
42 | (:err :*err*) *err*
43 | :auto (if (error-signal? signal) *err* *out*)
44 | stream)]
45 |
46 | (when-let [output (output-fn signal)]
47 | (.write stream (str output))
48 | (.flush stream))))))))
49 |
50 | :cljs
51 | (defn ^:public handler:console
52 | "Alpha, subject to change.
53 | If `js/console` exists, returns a signal handler that:
54 | - Takes a Telemere signal (map).
55 | - Writes the signal as a string to JavaScript console.
56 |
57 | A general-purpose `println`-style handler that's well suited for outputting
58 | signals as human or machine-readable (edn, JSON) strings.
59 |
60 | Options:
61 | `:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`"
62 |
63 | ([] (handler:console nil))
64 | ([{:keys [output-fn]
65 | :or {output-fn (utils/format-signal-fn)}}]
66 |
67 | (when (exists? js/console)
68 | (let [js-console-logger utils/js-console-logger]
69 |
70 | (fn a-handler:console
71 | ([ ]) ; Stop => noop
72 | ([signal]
73 | (when-let [output (output-fn signal)]
74 | (let [logger (js-console-logger (get signal :level))]
75 | (.call logger logger (str output)))))))))))
76 |
77 | #?(:cljs
78 | (defn- logger-fn [logger]
79 | ;; (fn [& xs] (.apply logger logger (into-array xs)))
80 | (fn
81 | ([x1 ] (.call logger logger x1))
82 | ([x1 x2 ] (.call logger logger x1 x2))
83 | ([x1 x2 x3 ] (.call logger logger x1 x2 x3))
84 | ([x1 x2 x3 & more] (apply logger x1 x2 x3 more)))))
85 |
86 | #?(:cljs
87 | (defn ^:public handler:console-raw
88 | "Alpha, subject to change.
89 | If `js/console` exists, returns a signal handler that:
90 | - Takes a Telemere signal (map).
91 | - Writes the raw signal to JavaScript console.
92 |
93 | Intended for use with browser formatting tools like `binaryage/devtools`,
94 | Ref. .
95 |
96 | Options:
97 | `:preamble-fn` ----- (fn [signal]) => string, see [1].
98 | `:format-nsecs-fn` - (fn [nanosecs]) => string.
99 |
100 | [1] `taoensso.telemere.utils/signal-preamble-fn`, etc."
101 |
102 | ([] (handler:console-raw nil))
103 | ([{:keys [preamble-fn format-nsecs-fn] :as opts
104 | :or
105 | {preamble-fn (utils/signal-preamble-fn)
106 | format-nsecs-fn (utils/format-nsecs-fn)}}]
107 |
108 | (when (and (exists? js/console) (exists? js/console.group))
109 | (let [js-console-logger utils/js-console-logger
110 | content-fn ; (fn [signal append-fn val-fn])
111 | (utils/signal-content-fn
112 | {:format-nsecs-fn format-nsecs-fn
113 | :format-error-fn nil
114 | :raw-error? true})]
115 |
116 | (fn a-handler:console-raw
117 | ([ ]) ; Stop => noop
118 | ([signal]
119 | (let [{:keys [level error]} signal
120 | logger (js-console-logger level)]
121 |
122 | ;; Unfortunately groups have no level
123 | (.group js/console (preamble-fn signal))
124 | (content-fn signal (logger-fn logger) identity)
125 |
126 | (when-let [stack (and error (.-stack (truss/ex-root error)))]
127 | (.call logger logger stack))
128 |
129 | (.groupEnd js/console)))))))))
130 |
--------------------------------------------------------------------------------
/main/src/taoensso/telemere/streams.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.telemere.streams
2 | "Standard streams -> Telemere interop."
3 | (:require
4 | [taoensso.encore :as truss]
5 | [taoensso.encore :as enc]
6 | [taoensso.telemere.impl :as impl]))
7 |
8 | (enc/defonce ^:private orig-*out* "Original `*out*` on ns load" *out*)
9 | (enc/defonce ^:private orig-*err* "Original `*err*` on ns load" *err*)
10 | (enc/defonce ^:no-doc ^:dynamic prev-*out* "Previous `*out*` (prior to any Telemere binds)" nil)
11 | (enc/defonce ^:no-doc ^:dynamic prev-*err* "Previous `*err*` (prior to any Telemere binds)" nil)
12 |
13 | (def ^:private ^:const default-out-opts {:kind :system/out, :level :info})
14 | (def ^:private ^:const default-err-opts {:kind :system/err, :level :error})
15 |
16 | (defn ^:no-doc osw
17 | "Private, don't use."
18 | ^java.io.OutputStreamWriter [x]
19 | (java.io.OutputStreamWriter. x))
20 |
21 | (defn ^:no-doc telemere-print-stream
22 | "Private, don't use.
23 | Returns a `java.io.PrintStream` that will flush to Telemere signals with given opts."
24 | ^java.io.PrintStream [{:as sig-opts :keys [kind level id]}]
25 | (let [baos
26 | (proxy [java.io.ByteArrayOutputStream] []
27 | (flush []
28 | (let [^java.io.ByteArrayOutputStream this this]
29 | (proxy-super flush)
30 | (let [msg (.trim (.toString this))]
31 | (proxy-super reset)
32 |
33 | (when-not (.isEmpty msg)
34 | (binding [*out* (or prev-*out* orig-*out*)
35 | *err* (or prev-*err* orig-*err*)]
36 |
37 | (impl/signal!
38 | {:ns nil
39 | :kind kind
40 | :level level
41 | :id id
42 | :msg msg})))))))]
43 |
44 | (java.io.PrintStream. baos true ; Auto flush
45 | java.nio.charset.StandardCharsets/UTF_8)))
46 |
47 | ;;;;
48 |
49 | (defmacro ^:public with-out->telemere
50 | "Executes form with `*out*` bound to flush to Telemere signals with given opts."
51 | ([ form] `(with-out->telemere nil ~form))
52 | ([opts form]
53 | `(binding [prev-*out* (or prev-*out* *out*)
54 | *out* (osw (telemere-print-stream ~(conj default-out-opts opts)))]
55 | ~form)))
56 |
57 | (defmacro ^:public with-err->telemere
58 | "Executes form with `*err*` bound to flush to Telemere signals with given opts."
59 | ([ form] `(with-err->telemere nil ~form))
60 | ([opts form]
61 | `(binding [prev-*err* (or prev-*err* *err*)
62 | *err* (osw (telemere-print-stream ~(conj default-err-opts opts)))]
63 | ~form)))
64 |
65 | (defmacro ^:public with-streams->telemere
66 | "Executes form with `*out*` and/or `*err*` bound to flush to Telemere signals
67 | with given opts."
68 | ([form] `(with-streams->telemere nil ~form))
69 | ([{:keys [out err]
70 | :or {out default-out-opts
71 | err default-err-opts}} form]
72 |
73 | `(binding [prev-*out* (or prev-*out* *out*)
74 | prev-*err* (or prev-*err* *err*)
75 | *out* (if-let [out# ~out] (osw (telemere-print-stream out#)) *out*)
76 | *err* (if-let [err# ~err] (osw (telemere-print-stream err#)) *err*)]
77 | ~form)))
78 |
79 | (comment (impl/with-signal (with-out->telemere (println "hello"))))
80 |
81 | (enc/defonce ^:private orig-out_ "Original `System/out`, or nil" (atom nil))
82 | (enc/defonce ^:private orig-err_ "Original `System/err`, or nil" (atom nil))
83 |
84 | (let [monitor (Object.)]
85 |
86 | (defn ^:public streams->reset!
87 | "Experimental, subject to change.
88 | Resets `System/out` and `System/err` to their original value (prior to any
89 | `streams->telemere!` call)."
90 | []
91 | (let [[orig-out _] (reset-vals! orig-out_ nil)
92 | [orig-err _] (reset-vals! orig-err_ nil)]
93 |
94 | (impl/signal!
95 | {:kind :event
96 | :level :info
97 | :id :taoensso.telemere/streams->telemere!
98 | :msg "Disabling interop: standard stream/s -> Telemere"
99 | :data {:system/out? (boolean orig-out)
100 | :system/err? (boolean orig-err)}})
101 |
102 | (locking monitor
103 | (when orig-out (System/setOut orig-out))
104 | (when orig-err (System/setErr orig-err)))
105 |
106 | (boolean (or orig-out orig-err))))
107 |
108 | (defn ^:public streams->telemere!
109 | "Experimental, subject to change.
110 |
111 | When given `out`, sets JVM's `System/out` to flush to Telemere signals with those opts.
112 | When given `err`, sets JVM's `System/err` to flush to Telemere signals with those opts.
113 |
114 | Note that setting `System/out` won't necessarily affect Clojure's `*out*`,
115 | and setting `System/err` won't necessarily affect Clojure's `*err*`.
116 |
117 | See also:
118 | `with-out->telemere`,
119 | `with-err->telemere`,
120 | `with-streams->telemere`."
121 |
122 | ([] (streams->telemere! nil))
123 | ([{:keys [out err]
124 | :or {out default-out-opts
125 | err default-err-opts}}]
126 |
127 | (when (or out err)
128 | (let [out (when out (telemere-print-stream out))
129 | err (when err (telemere-print-stream err))]
130 |
131 | (impl/signal!
132 | {:kind :event
133 | :level :info
134 | :id :taoensso.telemere/streams->telemere!
135 | :msg "Enabling interop: standard stream/s -> Telemere"
136 | :data {:system/out? (boolean out)
137 | :system/err? (boolean err)}})
138 |
139 | (locking monitor
140 | (when out (compare-and-set! orig-out_ nil System/out) (System/setOut out))
141 | (when err (compare-and-set! orig-err_ nil System/err) (System/setErr err)))
142 |
143 | true)))))
144 |
145 | (comment
146 | (streams->telemere?)
147 | (streams->telemere! {})
148 | (streams->reset!))
149 |
150 | ;;;;
151 |
152 | (defn check-out-interop
153 | "Returns interop debug info map."
154 | []
155 | (let [sending? (boolean @orig-out_)
156 | receiving? (and sending? (impl/test-interop! "`System/out` -> Telemere" #(.println System/out %)))]
157 | {:sending->telemere? sending?, :telemere-receiving? receiving?}))
158 |
159 | (defn check-err-interop
160 | "Returns interop debug info map."
161 | []
162 | (let [sending? (boolean @orig-err_)
163 | receiving? (and sending? (impl/test-interop! "`System/err` -> Telemere" #(.println System/err %)))]
164 | {:sending->telemere? sending?, :telemere-receiving? receiving?}))
165 |
166 | (impl/add-interop-check! :system/out check-out-interop)
167 | (impl/add-interop-check! :system/err check-err-interop)
168 |
--------------------------------------------------------------------------------
/wiki/7-Tips.md:
--------------------------------------------------------------------------------
1 | Building **observable systems** can be tough, there's no magic solutions. But the benefits can be large, often dramatically **outweighing the costs**.
2 |
3 | I'll present some basic/fundamental info here and can expand on this more in future if there's interest.
4 |
5 | # General guidance
6 |
7 | ## Consider what info you (will) need
8 |
9 | Try be as **concrete as possible** about what info is (or will be) most valuable about your system. **Get agreement on examples**.
10 |
11 | Info may be needed for:
12 |
13 | - Debugging
14 | - Business intelligence
15 | - Testing/staging/validation
16 | - Customer support
17 | - Quality management
18 | - Etc.
19 |
20 | Try be clear on:
21 |
22 | - Who *exactly* will need what information
23 | - In what time frame
24 | - In what form
25 | - And for what purpose (i.e. how will the information **be actionable**)
26 |
27 | Always try involve the **final consumer of information** in your design process.
28 |
29 | Always try map examples of **expected information** to **expected actionable decisions**, and document these mappings.
30 |
31 | The more clear the expected actionable decisions, the more clear the **information's value**.
32 |
33 | ## Consider data dependencies
34 |
35 | Not all data is inherently *useful* (and so valuable).
36 |
37 | Be clear on which data is:
38 |
39 | - Useful *independently*
40 | - Useful *in the aggregate* (e.g. statistically)
41 | - Useful *in association* with other related data (e.g. while tracing the flow of some logical activity)
42 |
43 | Remember to always **question assertions of usefulness**!!
44 |
45 | Useful for what **precise purpose** and by **whom**? Can a clear example be provided **mapping information to decisions**?
46 |
47 | When aggregates or associations are needed- does a plan exist for producing them from the raw data? Some forethought (e.g. appropriate identifiers and/or indexes) can help avoid big headaches!
48 |
49 | ## Consider cardinality
50 |
51 | Too much low-value data is often actively *harmful*: expensive to process, to store, and to query. Adding noise just interferes with better data, harming your ability to understand your system.
52 |
53 | Consider the **quantities** of data that'd best suit your needs *for that data*:
54 |
55 |
56 |
57 | Telemere offers [extensive filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) capabilities that help you easily express the **conditions** and **quantities** that make sense for your needs. *Use these* both for their effects and as a *form of documentation*.
58 |
59 | ## Consider evolution
60 |
61 | Both data and needs **evolve over time**.
62 |
63 | Consider **what is likely subject to change**, and how that might impact your observability needs and therefore design.
64 |
65 | Consider the **downstream effects** on data services/storage when something does change.
66 |
67 | **Use schemas** when appropriate. Use **version identifiers** when reasonable.
68 |
69 | Consider the [differences](https://www.youtube.com/watch?v=oyLBGkS5ICk) between **accretion** and **breakage**.
70 |
71 | # Telemere usage tips
72 |
73 | - [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) and [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) are both **good general-purpose** signal creators.
74 |
75 | - **Provide an id** for all signals you create.
76 |
77 | Qualified keywords are perfect! Downstream behaviour (e.g. alerts) can then look for these ids rather than messages (which are harder to match and more likely to change).
78 |
79 | - Keep a documented **index** of all your **signal ids** under version control.
80 |
81 | This way you can see all your ids in one place, and precise info on when ids were added/removed/changed.
82 |
83 | - Use **signal call transforms** to your advantage.
84 |
85 | The result of call-side signal transforms is cached and *shared between all handlers* making it an efficient place to modify signals going to >1 handler.
86 |
87 | - Signal and handler **sampling is multiplicative**.
88 |
89 | Both signals and handlers can have independent sample rates, and these MULTIPLY!
90 |
91 | If a signal is created with *20%* sampling and a handler handles *50%* of received signals, then *10%* of possible signals will be handled (50% of 20%).
92 |
93 | When sampling is active, the final (combined multiplicative) rate is helpfully reflected in each signal's `:sample` rate value ∈ℝ[0,1]. This makes it possible to estimate _unsampled_ cardinalities: for `n` randomly sampled signals matching some criteria, you'd have seen an estimated `Σ(1.0/sample-rate_i)` such signals _without_ sampling, etc.
94 |
95 | - Transforms can technically return any type, but it's best to return only `nil` or a map. This ensures maximum compatibility with community transforms, handlers, and tools.
96 |
97 | - Transforms can be used to **filter signals** by returning `nil`.
98 | - Transforms can be used to **split signals**:
99 |
100 | Your transforms can *call signal creators* like any other code. Return `nil` after to filter the source signal. Just be aware that new signals will re-enter your handler queue/s as would any other signal - and so may be subject to handling delay and normal handler queue back-pressure.
101 |
102 | See also the [`dispatch-signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#dispatch-signal!) util.
103 |
104 | - Levels can be **arbitrary integers**.
105 |
106 | See the value of [`level-aliases`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#level-aliases) to see how the standard keywords (`:info`, `:warn`, etc.) map to integers.
107 |
108 | - Signals with an `:error` value need not have `:error` level and vice versa.
109 |
110 | Telemere doesn't couple the presence of an error value to signal level. This can be handy, but means that you need to be clear on what constitutes an "error signal" for your use case. See also the [`error-signal?`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error-signal) util.
111 |
112 | - Signals may contain arbitrary app-level keys.
113 |
114 | Any non-standard [options](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) you give to a signal creator call will be added to the signal it creates:
115 |
116 | ```clojure
117 | (tel/with-signal (tel/log! {:my-key "foo"} "My message")))
118 | ;; => {:my-key "foo", :kvs {:my-key "foo", ...}, ...}
119 | ```
120 |
121 | Note that all app-level kvs will *also* be available *together* under the signal's `:kvs` key.
122 |
123 | App-level kvs are typically *not* included in handler output, so are a great way of providing custom data/opts for use (only) by custom transforms or handlers.
124 |
125 | - Signal `kind` can be useful in advanced cases.
126 |
127 | Every signal has a `kind` key set by default by each signal creator- `log!` calls produce signals with a `:log` kind, etc.
128 |
129 | Most folks won't use this, but it can be handy in advanced environments since it allows you to specify a custom taxonomy of signals separate from ids and namespaces.
130 |
131 | Signals can be filtered by kind, and minimum levels specified by kind.
--------------------------------------------------------------------------------
/slf4j/src/taoensso/telemere/slf4j.clj:
--------------------------------------------------------------------------------
1 | (ns taoensso.telemere.slf4j
2 | "SLF4Jv2 -> Telemere interop.
3 | Telemere will attempt to load this ns automatically when possible.
4 |
5 | To use Telemere as your SLF4J backend/provider, just include the
6 | `com.taoensso/telemere-slf4j` dependency on your classpath.
7 |
8 | Implementation details,
9 | Ref. :
10 |
11 | - Libs must include `org.slf4j/slf4j-api` dependency, but NO backend.
12 |
13 | - Users must include a single backend dependency of their choice
14 | (e.g. `com.taoensso/telemere-slf4j` or `org.slf4j/slf4j-simple`).
15 |
16 | - SLF4J uses standard `ServiceLoader` mechanism to find its logging backend,
17 | searches for `SLF4JServiceProvider` provider on classpath."
18 |
19 | {:author "Peter Taoussanis (@ptaoussanis)"}
20 | (:require
21 | [taoensso.truss :as truss]
22 | [taoensso.encore :as enc]
23 | [taoensso.telemere.impl :as impl])
24 |
25 | (:import
26 | [org.slf4j Logger]
27 | [com.taoensso.telemere.slf4j TelemereLogger]))
28 |
29 | (comment (remove-ns (symbol (str *ns*))))
30 |
31 | ;;;; Utils
32 |
33 | (defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))
34 |
35 | (defn- sig-level
36 | "Returns `taoensso.encore.signals` level for given `org.slf4j.event.Level`."
37 | ;; Faster than switching on `org.slf4j.event.EventConstants` directly
38 | [^org.slf4j.event.Level level]
39 | (enc/case-eval (.toInt level)
40 | org.slf4j.event.EventConstants/TRACE_INT :trace
41 | org.slf4j.event.EventConstants/DEBUG_INT :debug
42 | org.slf4j.event.EventConstants/INFO_INT :info
43 | org.slf4j.event.EventConstants/WARN_INT :warn
44 | org.slf4j.event.EventConstants/ERROR_INT :error
45 | (throw
46 | (ex-info "Unexpected `org.slf4j.event.Level`"
47 | {:level (enc/typed-val level)}))))
48 |
49 | (comment (enc/qb 1e6 (sig-level org.slf4j.event.Level/INFO))) ; 36.47
50 |
51 | (defn- get-marker "Private util for tests, etc."
52 | ^org.slf4j.Marker [n] (org.slf4j.MarkerFactory/getMarker n))
53 |
54 | (defn- est-marker!
55 | "Private util for tests, etc.
56 | Globally establishes (compound) `org.slf4j.Marker` with name `n` and mutates it
57 | (all occurences!) to have exactly the given references. Returns the (compound) marker."
58 | ^org.slf4j.Marker [n & refs]
59 | (let [m (get-marker n)]
60 | (enc/reduce-iterator! (fn [_ in] (.remove m in)) nil (.iterator m))
61 | (doseq [n refs] (.add m (get-marker n)))
62 | m))
63 |
64 | (comment [(est-marker! "a1" "a2") (get-marker "a1") (= (get-marker "a1") (get-marker "a1"))])
65 |
66 | (def ^:private get-marker-names
67 | "Returns #{}. Cached => assumes markers NOT modified after creation."
68 | ;; We use `BasicMarkerFactory` so:
69 | ;; 1. Our markers are just labels (no other content besides their name).
70 | ;; 2. Markers with the same name are identical (enabling caching).
71 | (enc/fmemoize
72 | (fn get-marker-names [marker-or-markers]
73 | (if (instance? org.slf4j.Marker marker-or-markers)
74 |
75 | ;; Single marker
76 | (let [^org.slf4j.Marker m marker-or-markers
77 | acc #{(.getName m)}]
78 |
79 | (if-not (.hasReferences m)
80 | acc
81 | (enc/reduce-iterator!
82 | (fn [acc ^org.slf4j.Marker in]
83 | (if-not (.hasReferences in)
84 | (conj acc (.getName in))
85 | (into acc (get-marker-names in))))
86 | acc (.iterator m))))
87 |
88 | ;; Vector of markers
89 | (reduce
90 | (fn [acc in] (into acc (get-marker-names in)))
91 | #{} (truss/have vector? marker-or-markers))))))
92 |
93 | (comment
94 | (let [m1 (est-marker! "M1")
95 | m2 (est-marker! "M1")
96 | cm (est-marker! "Compound" "M1" "M2")
97 | ms [m1 m2]]
98 |
99 | (enc/qb 1e6 ; [45.52 47.48 44.85]
100 | (get-marker-names m1)
101 | (get-marker-names cm)
102 | (get-marker-names ms))))
103 |
104 | ;;;; Interop fns (called by `TelemereLogger`)
105 |
106 | (defn- allowed?
107 | "Called by `com.taoensso.telemere.slf4j.TelemereLogger`."
108 | [logger-name level]
109 | (when-debug (println [:slf4j/allowed? (sig-level level) logger-name]))
110 | (impl/signal-allowed?
111 | {:ns logger-name ; Typically source class name
112 | :kind :slf4j
113 | :level (sig-level level)}))
114 |
115 | (defn- normalized-log!
116 | [logger-name level inst error msg-pattern args marker-names kvs]
117 | (when-debug (println [:slf4j/normalized-log! (sig-level level) logger-name]))
118 | (impl/signal!
119 | {:allow? true ; Pre-filtered by `allowed?` call
120 | :ns logger-name ; Typically source class name
121 | :kind :slf4j
122 | :level (sig-level level)
123 | :inst inst
124 | :error error
125 |
126 | :ctx+
127 | (when-let [hmap (org.slf4j.MDC/getCopyOfContextMap)]
128 | (clojure.lang.PersistentHashMap/create hmap))
129 |
130 | :msg
131 | (delay
132 | (org.slf4j.helpers.MessageFormatter/basicArrayFormat
133 | msg-pattern args))
134 |
135 | :slf4j/args args ; Object[]
136 | :slf4j/markers marker-names ; Usu. used for routing, filtering, xfns, etc.
137 | :data (when kvs {:slf4j/kvs kvs})})
138 |
139 | nil)
140 |
141 | (defn- log!
142 | "Called by `com.taoensso.telemere.slf4j.TelemereLogger`."
143 |
144 | ;; Modern "fluent" API calls
145 | ([logger-name ^org.slf4j.event.LoggingEvent event]
146 | (let [inst (or (when-let [ts (.getTimeStamp event)] (java.time.Instant/ofEpochMilli ts)) (enc/now-inst*))
147 | level (.getLevel event)
148 | error (.getThrowable event)
149 | msg-pattern (.getMessage event)
150 | args (when-let [args (.getArgumentArray event)] args)
151 | marker-names (when-let [markers (.getMarkers event)] (get-marker-names (vec markers)))
152 | kvs (when-let [kvps (.getKeyValuePairs event)]
153 | (reduce
154 | (fn [acc ^org.slf4j.event.KeyValuePair kvp]
155 | (assoc acc (.-key kvp) (.-value kvp)))
156 | nil kvps))]
157 |
158 | (when-debug (println [:slf4j/fluent-log-call (sig-level level) logger-name]))
159 | (normalized-log! logger-name level inst error msg-pattern args marker-names kvs)))
160 |
161 | ;; Legacy API calls
162 | ([logger-name ^org.slf4j.event.Level level error msg-pattern args marker]
163 | (let [marker-names (when marker (get-marker-names marker))]
164 | (when-debug (println [:slf4j/legacy-log-call (sig-level level) logger-name]))
165 | (normalized-log! logger-name level (enc/now-inst*) error msg-pattern args marker-names nil))))
166 |
167 | (comment
168 | (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "my.class"))
169 | (impl/with-signal (-> sl (.info "Hello {}" "x")))
170 | (impl/with-signal (-> (.atInfo sl) (.log "Hello {}" "x")))
171 |
172 | (do ; Will noop with `NOPMDCAdapter`
173 | (org.slf4j.MDC/put "key" "val")
174 | (org.slf4j.MDC/get "key")
175 | (org.slf4j.MDC/getCopyOfContextMap)
176 | (org.slf4j.MDC/clear)))
177 |
178 | ;;;;
179 |
180 | (defn check-interop
181 | "Returns interop debug info map."
182 | []
183 | (let [^org.slf4j.Logger sl
184 | (org.slf4j.LoggerFactory/getLogger "InteropTestTelemereLogger")
185 | sending? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl)
186 | receiving?
187 | (and sending?
188 | (impl/test-interop! "SLF4J -> Telemere" #(.info sl %)))]
189 |
190 | {:present? true
191 | :telemere-provider-present? true
192 | :sending->telemere? sending?
193 | :telemere-receiving? receiving?}))
194 |
195 | (impl/add-interop-check! :slf4j check-interop)
196 |
197 | (impl/on-init
198 | (impl/signal!
199 | {:kind :event
200 | :level :debug ; < :info since runs on init
201 | :id :taoensso.telemere/slf4j->telemere!
202 | :msg "Enabling interop: SLF4J -> Telemere"}))
203 |
--------------------------------------------------------------------------------
/imgs/telemere-logo.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/wiki/3-Config.md:
--------------------------------------------------------------------------------
1 | See below for config by topic-
2 |
3 | # Filtering
4 |
5 | A signal will be provided to a handler iff **ALL** of the following are true:
6 |
7 | - 1. Signal **call filters** pass:
8 | - a. Compile time: sample rate, kind, ns, id, level, when form, rate limit
9 | - b. Runtime: sample rate, kind, ns, id, level, when form, rate limit
10 |
11 | - 2. Signal **handler filters** pass:
12 | - a. Compile time: not applicable
13 | - b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit
14 |
15 | - 3. **Call transform** `(fn [signal]) => ?modified-signal` returns non-nil
16 | - 4. **Handler transform** `(fn [signal]) => ?modified-signal` returns non-nil
17 |
18 | > 👉 Transform fns provides a flexible way to modify and/or filter signals by arbitrary signal data/content conditions (return nil to skip handling).
19 |
20 | > 👉 Call and handler filters are **additive** - so handlers can be *more* but not *less* restrictive than call filters allow. This makes sense: call filters decide if a signal can be created. Handler filters decide if a particular handler is allowed to handle a created signal.
21 |
22 | See [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) for more about filtering.
23 |
24 | ## Debugging filters
25 |
26 | Telemere offers a *lot* of filtering control, so real systems can get quite complex. There's a lot of tools to help debug, including:
27 |
28 | | Util | |
29 | | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
30 | | [`with-signal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signal) | To see *last* signal created in body |
31 | | [`with-signals`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signals) | To see *all* signals created in body |
32 | | [`get-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters) | To see all call filters in current context |
33 | | [`without-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#without-filters) | To disable filters in body |
34 | | [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) | To see handler call stats |
35 |
36 | # Signal handlers
37 |
38 | See section [4-Handlers](./4-Handlers).
39 |
40 | # Interop
41 |
42 | ## tools.logging
43 |
44 | [tools.logging](https://github.com/clojure/tools.logging) can use Telemere as its logging implementation (backend). This'll let tools.logging calls create Telemere signals.
45 |
46 | To do this:
47 |
48 | 1. Ensure that you have the tools.logging [dependency](https://mvnrepository.com/artifact/org.clojure/tools.logging), and
49 | 2. Call [`tools-logging->telemere!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.tools-logging#tools-logging-%3Etelemere!), or set the relevant environmental config as described in its docstring.
50 |
51 | Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
52 |
53 | ```clojure
54 | (check-interop) ; =>
55 | {:tools-logging {:sending->telemere? true, :telemere-receiving? true}}
56 | ```
57 |
58 | ## Java logging
59 |
60 | [SLF4Jv2](https://www.slf4j.org/) can use Telemere as its logging backend. This'll let SLF4J logging calls create Telemere signals.
61 |
62 | To do this:
63 |
64 | 1. Ensure that you have the SLF4J [dependency](https://mvnrepository.com/artifact/org.slf4j/slf4j-api) (v2+ **only**), and
65 | 2. Ensure that you have the Telemere SLF4J backend [dependency](https://clojars.org/com.taoensso/telemere-slf4j)
66 |
67 | When `com.taoensso/telemere-slf4j` (2) is on your classpath AND no other SLF4J backends are, SLF4J will automatically direct all its logging calls to Telemere.
68 |
69 | Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
70 |
71 | ```clojure
72 | (check-interop) ; =>
73 | {:slf4j {:sending->telemere? true, :telemere-receiving? true}}
74 | ```
75 |
76 | > Telemere needs SLF4J API **version 2 or newer**. If you're seeing `Failed to load class "org.slf4j.impl.StaticLoggerBinder"` it could be that your project is importing the older v1 API, check with `lein deps :tree` or equivalent.
77 |
78 | For other (non-SLF4J) logging like [Log4j](https://logging.apache.org/log4j/2.x/), [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) (JUL), and [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/) (JCL), use an appropriate [SLF4J bridge](https://www.slf4j.org/legacy.html) and the normal SLF4J config as above.
79 |
80 | In this case logging will be forwarded:
81 |
82 | 1. From Log4j/JUL/JCL/etc. to SLF4J, and
83 | 2. From SLF4J to Telemere
84 |
85 | ## System streams
86 |
87 | The JVM's `System/out` and/or `System/err` streams can be set so that they'll create Telemere signals when flushed.
88 |
89 | To do this, call [`streams->telemere!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#streams-%3Etelemere!).
90 |
91 | Note that Clojure's `*out*`, `*err*` are **not** necessarily automatically affected.
92 |
93 | Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
94 |
95 | ```clojure
96 | (check-interop) ; =>
97 | {:system/out {:sending->telemere? true, :telemere-receiving? true}
98 | :system/err {:sending->telemere? true, :telemere-receiving? true}}
99 | ```
100 |
101 | ## OpenTelemetry
102 |
103 | > [OpenTelemetry](https://opentelemetry.io/) is a popular open-source observability framework that provides tools for collecting, processing, and exporting telemetry data like traces, metrics, and logs from software systems.
104 | >
105 | > Telemere's OpenTelemetry interop is **experimental** - I'm looking for [feedback](https://www.taoensso.com/telemere/slack) on this feature please! 🙏
106 |
107 | Telemere can send signals as [`LogRecords`](https://opentelemetry.io/docs/specs/otel/logs/data-model/) with correlated tracing data to configured [OpenTelemetry Java](https://github.com/open-telemetry/opentelemetry-java) [exporters](https://opentelemetry.io/docs/languages/java/exporters/).
108 |
109 | This allows output to go (via configured exporters) to a wide variety of targets like [Jaeger](https://www.jaegertracing.io/), [Zipkin](https://zipkin.io/), [AWS X-Ray](https://aws.amazon.com/xray/), [AWS CloudWatch](https://aws.amazon.com/cloudwatch/), etc.
110 |
111 | To do this:
112 |
113 | 1. Ensure that you have the necessary [OpenTelemetry Java](https://github.com/open-telemetry/opentelemetry-java) [dependency](https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-api).
114 | 2. Ensure that the relevant exporters are [appropriately configured](https://opentelemetry.io/docs/languages/java/configuration/) (this is the trickiest part, but not at all specific to Telemere).
115 | 3. Create a Telemere signal handler using [`handler:open-telemetry`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry), and register it using [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!).
116 | 4. Ensure that [`otel-tracing?`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#otel-tracing?) is enabled if you want tracing interop.
117 |
118 | Aside from configuring the exporters (2), Telemere's OpenTelemetry interop **does not require** any use of or familiarity with the OpenTelemetry Java API or concepts. Just use Telemere as you normally would, and the handler (3) will automatically emit detailed log and trace data to your configured exporters (2).
119 |
120 | Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
121 |
122 | ```clojure
123 | (check-interop) ; =>
124 | {:open-telemetry {:present? true, :use-tracer? true, :viable-tracer? true}}
125 | ```
126 |
127 | ## Tufte
128 |
129 | > [Tufte](https://www.taoensso.com/tufte) is a simple performance monitoring library for Clojure/Script by the author of Telemere.
130 |
131 | Telemere can easily incorporate Tufte performance data in its signals, just like any other data:
132 |
133 | ```clojure
134 | (let [[_ perf-data] (tufte/profiled