diff --git a/projects/api/.gitignore b/projects/api/.gitignore new file mode 100644 index 0000000..f9929ef --- /dev/null +++ b/projects/api/.gitignore @@ -0,0 +1,15 @@ +pom.xml* +.lein* +.nrepl-port +*.jar +*.class +.env +.DS_Store +/lib/ +/classes/ +/target/ +/checkouts/ +/logs/ +/.clj-kondo/.cache +.idea/ +*.iml diff --git a/projects/api/project.clj b/projects/api/project.clj new file mode 100644 index 0000000..7cb81a4 --- /dev/null +++ b/projects/api/project.clj @@ -0,0 +1,44 @@ +(defproject com.taoensso/telemere-api "1.0.0-SNAPSHOT" + :author "Peter Taoussanis " + :description "Minimal Telemere facade API for library authors, etc." + :url "https://www.taoensso.com/telemere" + + :license + {:name "Eclipse Public License - v 1.0" + :url "https://www.eclipse.org/legal/epl-v10.html"} + + :scm {:name "git" :url "https://github.com/taoensso/telemere"} + + :dependencies [] + :profiles + {:provided + {:dependencies + [[org.clojure/clojurescript "1.11.132"] + [org.clojure/clojure "1.11.4"] + [com.taoensso/telemere "1.0.0-SNAPSHOT"]]} + + :dev + {:plugins + [[lein-pprint "1.3.2"] + [lein-ancient "0.7.0"] + [lein-cljsbuild "1.1.8"]]}} + + :cljsbuild + {:test-commands {"node" ["node" "target/test.js"]} + :builds + [{:id :main + :source-paths ["src"] + :compiler + {:output-to "target/main.js" + :optimizations :advanced}} + + {:id :test + :source-paths ["src" "test"] + :compiler + {:output-to "target/test.js" + :target :nodejs + :optimizations :simple}}]} + + :aliases + {"build-once" ["do" ["clean"] ["cljsbuild" "once"]] + "deploy-lib" ["do" ["build-once"] ["deploy" "clojars"] ["install"]]}) diff --git a/projects/api/src/taoensso/telemere/api.cljc b/projects/api/src/taoensso/telemere/api.cljc new file mode 100644 index 0000000..cc171ff --- /dev/null +++ b/projects/api/src/taoensso/telemere/api.cljc @@ -0,0 +1,153 @@ +(ns taoensso.telemere.api + "Experimental, subject to change. + Minimal Telemere facade API for library authors, etc. + Allows library code to use Telemere if it's present, or fall back to + something like `tools.logging` otherwise. + + (ns my-lib + (:require + [taoensso.telemere.api :as t] ; `com.taoensso/telemere-api` dependency + [clojure.tools.logging :as ctl] ; `org.clojure/tools.logging` dependency + )) + + (t/require-telemere-if-present) ; Just below `ns` form + + ;; Optional convenience for library users + (defn set-min-level! + \"If using Telemere, sets Telemere's minimum level for namespaces. + Possible levels: #{:trace :debug :info :warn :error :fatal :report}. + Default level: `:warn`. + [min-level] + (t/if-telemere + (do (t/set-min-level! nil \"my-lib(.*)\" min-level) true) + false)) + + (defonce ^:private __set-default-min-level (set-min-level! :warn)) + + (signal! + {:kind :log, :id :my-id, :level :warn, + :let [x :x] + :msg [\"Hello\" \"world\" x] + :data {:a :A :x x} + :fallback (ctl/warn (str \"Hello world\" x))})" + + {:author "Peter Taoussanis (@ptaoussanis)"} + #?(:clj (:require [clojure.java.io :as jio]) + :cljs (:require-macros [taoensso.telemere.api :refer [compile-if]]))) + +(comment + (require '[taoensso.telemere :as t] '[taoensso.encore :as enc]) + (remove-ns 'taoensso.telemere.api) + (:api (enc/interns-overview))) + +#?(:clj + (defmacro ^:private compile-if [test then else] + (if (try (eval test) (catch Throwable _ false)) then else))) + +(def ^:private telemere-present? + "Is Telemere present (not necessarily loaded)?" + (compile-if (jio/resource "taoensso/telemere.cljc") true false)) + +#?(:clj + (defmacro if-telemere + "Evaluates to `then` form if Telemere is present when expanding macro. + Otherwise expands to `else` form." + ([then ] (if telemere-present? then nil)) + ([then else] (if telemere-present? then else)))) + +#?(:clj + (defmacro require-telemere-if-present + "Experimental, subject to change. + Requires Telemere if it's present, otherwise noops. + Used in cooperation with `signal!`, etc. + + For Cljs: + - MUST be placed at top of file, just below `ns` form + - Needs ClojureScript >= v1.9.293 (Oct 2016)" + [] (if-telemere `(require 'taoensso.telemere)))) + +(comment (require-telemere-if-present)) + +#?(:clj + (defmacro set-min-level! + "Expands to `taoensso.telemere/set-min-level!` call iff Telemere is present. + Otherwise expands to `nil` (noops)." + ([ min-level] (if-telemere `(taoensso.telemere/set-min-level! ~min-level))) + ([kind min-level] (if-telemere `(taoensso.telemere/set-min-level! ~kind ~min-level))) + ([kind ns-filter min-level] (if-telemere `(taoensso.telemere/set-min-level! ~kind ~ns-filter ~min-level))))) + +(comment (set-min-level! nil "my-ns(.*)" :warn)) + +#?(:clj + (defmacro signal! + "Experimental, subject to change. + Telemere facade API for library authors, etc. + + Expands to `taoensso.telemere/signal!` call if Telemere is present. + Otherwise expands to arbitrary `fallback` opt form. + + Allows library code to use Telemere if it's present, or fall back to + something like `tools.logging` otherwise. + + MUST be used with `require-telemere-if-present`, example: + + (ns my-lib + (:require + [taoensso.telemere.api :as t] ; `com.taoensso/telemere-api` dependency + [clojure.tools.logging :as ctl] ; `org.clojure/tools.logging` dependency + )) + + (t/require-telemere-if-present) ; Just below `ns` form! + + (t/signal! + {:kind :log, :id :my-id, :level :warn, + :let [x :x] + :msg [\"Hello\" \"world\" x] + :data {:a :A :x x} + :fallback (ctl/warn (str \"Hello world\" x))}) + + For more info, see: + - Telemere `signal!`, Ref. + - Telemere docs, Ref. " + + {:arglists #_(taoensso.telemere.impl/signal-arglists :signal!) ; + fallback + '([{:as opts + :keys + [fallback, ; Unique to facade + #_defaults #_elide? #_allow? #_expansion-id, ; Undocumented + elidable? location #_location* inst uid middleware, + sample-rate kind ns id level when rate-limit, + ctx parent root trace?, do let data msg error run & kvs]}])} + + [opts] + (if (map? opts) + (if telemere-present? + (do + (try + (require 'taoensso.telemere) ; For macro expansion + (catch Exception e + (throw + (ex-info "Failed to require `taoensso.telemere` - `(require-telemere-if-present)` call missing?" + {} e)))) + + (with-meta ; Keep callsite + `(taoensso.telemere/signal! ~(dissoc opts :fallback)) + (meta &form))) + + (let [fb-form (get opts :fallback)] + (if-let [run-form (get opts :run)] + `(let [run-result# ~run-form] ~fb-form run-result#) + (do fb-form)))) + + (throw + (ex-info "`signal!` expects map opts" + {:given {:value opts, :type (type opts)}}))))) + +(comment + (macroexpand + '(signal! + {:kind :log, :id :my-id, :level :warn, + :let [x :x] + :msg ["Hello" "world" x] + :data {:a :A :x x} + :fallback (println (str "Hello world " x))}))) diff --git a/projects/main/project.clj b/projects/main/project.clj index 501644e..8509398 100644 --- a/projects/main/project.clj +++ b/projects/main/project.clj @@ -49,6 +49,7 @@ [[org.clojure/test.check "1.1.1"] [org.clojure/tools.logging "1.3.0"] [org.slf4j/slf4j-api "2.0.16"] + [com.taoensso/telemere-api "1.0.0-SNAPSHOT"] [com.taoensso/slf4j-telemere "1.0.0-SNAPSHOT"] #_[org.slf4j/slf4j-simple "2.0.16"] #_[org.slf4j/slf4j-nop "2.0.16"] diff --git a/projects/main/test/taoensso/telemere_tests.cljc b/projects/main/test/taoensso/telemere_tests.cljc index 969bf57..3662c2c 100644 --- a/projects/main/test/taoensso/telemere_tests.cljc +++ b/projects/main/test/taoensso/telemere_tests.cljc @@ -8,6 +8,7 @@ :refer [signal! with-signal with-signals] :rename {signal! sig!, with-signal with-sig, with-signals with-sigs}] + [taoensso.telemere.api :as api] [taoensso.telemere.utils :as utils] [taoensso.telemere.timbre :as timbre] #_[taoensso.telemere.tools-logging :as tools-logging] @@ -271,7 +272,9 @@ (update-in [:inst] enc/inst->udt) (update-in [:end-inst] enc/inst->udt))] - [(is (= sv1 (read-string (pr-str sv1))))])))]) + [(is (= sv1 (read-string (pr-str sv1))))]))) + + (is (sm? (with-sig (api/signal! {:level :info})) {:level :info, :ns "taoensso.telemere-tests", :line :submap/some}) "API")]) (deftest _handlers ;; Basic handler tests are in Encore diff --git a/wiki/6-FAQ.md b/wiki/6-FAQ.md index 237d373..159d32b 100644 --- a/wiki/6-FAQ.md +++ b/wiki/6-FAQ.md @@ -113,7 +113,7 @@ See also [`msg-skip`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/tao # How to use Telemere from a library? -See section [9-Maintainers](./9-Maintainers). +See section [9-Authors](./9-Authors.md). # How does Telemere compare to Mulog? diff --git a/wiki/9-Authors.md b/wiki/9-Authors.md new file mode 100644 index 0000000..3565ff6 --- /dev/null +++ b/wiki/9-Authors.md @@ -0,0 +1,46 @@ +Are you a library author/maintainer that's considering **using Telemere in your library**? + +# Options +## 1. Consider a basic facade + +Does your library **really need** Telemere? Many libraries only need very basic logging. In these cases it can be beneficial to do your logging through a common basic facade like [tools.logging](https://github.com/clojure/tools.logging) or [SLF4J](https://www.slf4j.org/). + +**Pro**: users can then choose and configure their **preferred backend** - including Telemere, which can easily [act as a backend](./3-Config#interop) for both tools.logging and SLF4J. + +**Cons**: you'll be limited by what your facade API offers, and so lose support for Telemere's advanced features like structured logging, [rich filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters), etc. + +## 2. Telemere as a transitive dependency + +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. + +Telemere's [default config](./1-Getting-started#default-config) is sensible (with println-like console output), so many of library users won't need to configure or interact with Telemere at all. + +The most common thing library users may want to do is **adjust the minimum level** of signals created by your library. And since your users might not be familiar with Telemere, I'd recommend including something like the following in a convenient place like your library's main API namespace: + +```clojure +(defn set-min-log-level! + "Sets Telemere's minimum level for namespaces. + This will affect all signals (logs) created by . + + Possible minimum levels (from most->least verbose): + #{:trace :debug :info :warn :error :fatal :report}. + + The default minimum level is `:warn`." + [min-level] + (tel/set-min-level! nil "my-lib-ns(.*)" min-level) + true) + +(defonce ^:private __set-default-log-level (set-min-log-level! :warn)) +``` + +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. + +## 3. Telemere as an optional dependency + +Include the (super lightweight) [Telemere facade API](https://clojars.org/com.taoensso/telemere-api) in your **library's dependencies**. + +Your library will then be able to take advantage of Telemere **when Telemere is present**, or fall back to something like [tools.logging](https://github.com/clojure/tools.logging) otherwise. + +The main trade-off is that your signal calls will be more verbose. + +See [here](https://cljdoc.org/d/com.taoensso/telemere-api/CURRENT/api/taoensso.telemere.api) for an example and more info. \ No newline at end of file diff --git a/wiki/9-Maintainers.md b/wiki/9-Maintainers.md deleted file mode 100644 index a46d760..0000000 --- a/wiki/9-Maintainers.md +++ /dev/null @@ -1,78 +0,0 @@ -Are you a library maintainer that's considering **using Telemere in your library**? - -See below for some considerations and advice- - -# Consider a facade - -Ask yourself the question: do you specifically *need/want* Telemere? - -Many libraries only need very basic logging. In these cases it can be beneficial to do your logging through a facade like [tools.logging](https://github.com/clojure/tools.logging) or [SLF4J](https://www.slf4j.org/). - -**Upside**: users can then easily choose and configure their **preferred backend** (including Telemere, which can easily [act as a backend](./3-Config#interop) for both tools.logging and SLF4J). - -**Downside**: your logging features will necessarily be limited to the lowest-common-denominator features supported by your chosen facade (tools.logging or SLF4J, etc.). In particular, you'll be giving up support for Telemere's structured logging features and [rich filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters) (including filtering or setting minimum levels by namespaces). - -# Consider API stability - -Telemere is still currently in **beta** as of May 2024. - -While the [signal creator](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) and [signal content](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) APIs should already be mostly stable, I would still **recommend against** using Telemere in any public libraries until after Telemere's **stable v1 release** (current ETA >= [August 2024](https://taoensso.com/roadmap)). - -# Using Telemere in your library - -If you **do** need/want support for Telemere's structured logging features and/or [rich filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters), then you've got a couple options- - -## Telemere as a non-optional dependency - -This is straight-forward: you include Telemere in your library's dependencies, and you can make use of Telemere's full API from your own library. - -Telemere's [default config](./1-Getting-started#default-config) is sensible (with println-like console output), so many of library users won't need to configure or interact with Telemere at all. - -The most common thing library users may want to do is **adjust the minimum level** of signals created by your library. And since your users might not be familiar with Telemere, I'd recommend including something like the following in a convenient place like your library's main API namespace: - -```clojure -(defn set-min-log-level! - "Sets minimum level of Telemere signals (logs) created by . - Possible levels (from most to least verbose): - #{:trace :debug :info :warn :error :fatal :report}. - - The default level is `:warn`." - [level] - (tel/set-min-level! nil "my-library.namespace" min-level) - (tel/set-min-level! nil "my-library.namespace.*" min-level) - nil) - -(defonce ^:private __set-default-log-level (set-min-log-level! :warn)) -``` - -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. - -## Telemere as an optional dependency - -I have a solution planned for this that I'm still testing. Will add more info prior to Telemere's [stable v1 release](https://www.taoensso.com/roadmap). - -# Migrating from Timbre - -Do you have a library that currently uses [Timbre](https://www.taoensso.com/timbre), and you're considering a change to Telemere? - -## With a facade - -First, I'd encourage you to [consider a facade](#consider-a-fascade). - -Unless you specifically want features *only* available to Telemere/Timbre, using a facade would give your users the option to choose **whichever backend they prefer**. - -Since both Telemere and Timbre support [tools.logging](https://github.com/clojure/tools.logging) and [SLF4J](https://www.slf4j.org/), either one of those would be a reasonable choice. - -(Though note that Timbre's current SLF4J support is a little fragile. Timbre v7 [will introduce](https://github.com/taoensso/roadmap/issues/11) improved native SLF4J support, so you may want to wait for that if you are considering SLF4J). - -Example migration steps: - -1. Migrate your library's Timbre logging calls to equivalent [`tools.logging`](https://github.com/clojure/tools.logging) calls. -2. Remove your library's Timbre dependency. -3. Advise your users about the dropped dependency, and tell them that they'll now need to opt-in to [use Telemere](https://github.com/taoensso/telemere/wiki/3-Config#toolslogging), [use Timbre](https://taoensso.github.io/timbre/taoensso.timbre.tools.logging.html#var-use-timbre), or use some other logging backend for tools.logging that they prefer. - -## Without a facade - -In this case you'll need to decide if you want to use Telemere as an [optional](#telemere-as-an-optional-dependency) or [non-optional](#telemere-as-a-non-optional-dependency) dependency. - -Will add more info prior to Telemere's [stable v1 release](https://www.taoensso.com/roadmap).