├── .github └── workflows │ ├── clj-tests.yml │ ├── cljs-tests.yml │ └── graal-tests.yml ├── CHANGELOG.md ├── FUNDING.yml ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── doc └── cljdoc.edn ├── examples.cljc ├── handlers ├── consoles.cljc ├── files.clj ├── open_telemetry.clj ├── postal.clj ├── slack.clj └── sockets.clj ├── imgs ├── handler-output-clj-file.png ├── handler-output-cljs-console-raw.png ├── handler-output-cljs-console.png ├── signal-flow.svg ├── signal-sampling.svg ├── telemere-logo.svg └── telemere.fig ├── install.sh ├── main ├── .gitignore ├── bb.edn ├── bb │ └── graal_tests.clj ├── jaeger.sh ├── project.clj ├── public │ └── index.html ├── resources │ └── docs │ │ ├── catch-to-error!.txt │ │ ├── environmental-config.txt │ │ ├── error!.txt │ │ ├── event!.txt │ │ ├── log!.txt │ │ ├── signal!.txt │ │ ├── signal-content.txt │ │ ├── signal-creators.txt │ │ ├── signal-options.txt │ │ ├── spy!.txt │ │ └── trace!.txt ├── shadow-cljs.edn ├── shadow-cljs.sh ├── src │ └── taoensso │ │ ├── telemere.cljc │ │ └── telemere │ │ ├── consoles.cljc │ │ ├── files.clj │ │ ├── impl.cljc │ │ ├── open_telemetry.clj │ │ ├── postal.clj │ │ ├── slack.clj │ │ ├── sockets.clj │ │ ├── streams.clj │ │ ├── timbre.cljc │ │ ├── tools_logging.clj │ │ └── utils.cljc └── test │ └── taoensso │ ├── graal_tests.clj │ └── telemere_tests.cljc ├── slf4j ├── .gitignore ├── project.clj ├── resources │ └── META-INF │ │ └── services │ │ └── org.slf4j.spi.SLF4JServiceProvider └── src │ ├── java │ └── com │ │ └── taoensso │ │ └── telemere │ │ └── slf4j │ │ ├── TelemereLogger.java │ │ ├── TelemereLoggerFactory.java │ │ └── TelemereServiceProvider.java │ └── taoensso │ └── telemere │ └── slf4j.clj └── wiki ├── .gitignore ├── 1-Getting-started.md ├── 2-Architecture.md ├── 3-Config.md ├── 4-Handlers.md ├── 5-Migrating.md ├── 6-FAQ.md ├── 7-Tips.md ├── 8-Community.md ├── 9-Authors.md ├── Home.md └── README.md /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ptaoussanis 2 | custom: "https://www.taoensso.com/clojure" 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc/docstring-format :plaintext} 2 | 3 | -------------------------------------------------------------------------------- /examples.cljc: -------------------------------------------------------------------------------- 1 | (ns examples 2 | "Basic Telemere usage examples that appear in the Wiki or docstrings." 3 | (:require [taoensso.telemere :as tel])) 4 | 5 | (comment 6 | 7 | ;;;; README "Quick examples" 8 | 9 | (require '[taoensso.telemere :as tel]) 10 | 11 | ;; No config needed for typical use cases!! 12 | ;; Signals print to console by default for both Clj and Cljs 13 | 14 | ;; Traditional style logging (data formatted into message string): 15 | (tel/log! {:level :info, :msg (str "User " 1234 " logged in!")}) 16 | 17 | ;; Modern/structured style logging (explicit id and data) 18 | (tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}}) 19 | 20 | ;; Mixed style (explicit id and data, with message string) 21 | (tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}, :msg "User logged in!"}) 22 | 23 | ;; Trace (can interop with OpenTelemetry) 24 | ;; Tracks form runtime, return value, and (nested) parent tree 25 | (tel/trace! {:id ::my-id :data {...}} 26 | (do-some-work)) 27 | 28 | ;; Check resulting signal content for debug/tests 29 | (tel/with-signal (tel/log! {...})) ; => {:keys [ns level id data msg_ ...]} 30 | 31 | ;; Getting fancy (all costs are conditional!) 32 | (tel/log! 33 | {:level :debug 34 | :sample 0.75 ; 75% sampling (noop 25% of the time) 35 | :when (my-conditional) 36 | :limit {"1 per sec" [1 1000] 37 | "5 per min" [5 60000]} ; Rate limit 38 | :limit-by my-user-ip-address ; Rate limit scope 39 | 40 | :do (inc-my-metric!) 41 | :let 42 | [diagnostics (my-expensive-diagnostics) 43 | formatted (my-expensive-format diagnostics)] 44 | 45 | :data 46 | {:diagnostics diagnostics 47 | :formatted formatted 48 | :local-state *my-dynamic-context*}} 49 | 50 | ;; Message string or vector to join as string 51 | ["Something interesting happened!" formatted]) 52 | ) 53 | 54 | ;; Set minimum level 55 | (tel/set-min-level! :warn) ; For all signals 56 | (tel/set-min-level! :log :debug) ; For `log!` signals specifically 57 | 58 | ;; Set id and namespace filters 59 | (tel/set-id-filter! {:allow #{::my-particular-id "my-app/*"}}) 60 | (tel/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"}) 61 | 62 | ;; SLF4J signals will have their `:ns` key set to the logger's name 63 | ;; (typically a source class) 64 | (tel/set-ns-filter! {:disallow "com.noisy.java.package.*"}) 65 | 66 | ;; Set minimum level for `log!` signals for particular ns pattern 67 | (tel/set-min-level! :log "taoensso.sente.*" :warn) 68 | 69 | ;; Use transforms (xfns) to filter and/or arbitrarily modify signals 70 | ;; by signal data/content/etc. 71 | 72 | (tel/set-xfn! 73 | (fn [signal] 74 | (if (-> signal :data :skip-me?) 75 | nil ; Filter signal (don't handle) 76 | (assoc signal :transformed? true)))) 77 | 78 | (tel/with-signal (tel/log! {... :data {:skip-me? true}})) ; => nil 79 | (tel/with-signal (tel/log! {... :data {:skip-me? false}})) ; => {...} 80 | 81 | ;; See `tel/help:filters` docstring for more filtering options 82 | 83 | ;; Add your own signal handler 84 | (tel/add-handler! :my-handler 85 | (fn 86 | ([signal] (println signal)) 87 | ([] (println "Handler has shut down")))) 88 | 89 | ;; Use `add-handler!` to set handler-level filtering and back-pressure 90 | (tel/add-handler! :my-handler 91 | (fn 92 | ([signal] (println signal)) 93 | ([] (println "Handler has shut down"))) 94 | 95 | {:async {:mode :dropping, :buffer-size 1024, :n-threads 1} 96 | :priority 100 97 | :sample 0.5 98 | :min-level :info 99 | :ns-filter {:disallow "taoensso.*"} 100 | :limit {"1 per sec" [1 1000]} 101 | ;; See `tel/help:handler-dispatch-options` for more 102 | }) 103 | 104 | ;; See current handlers 105 | (tel/get-handlers) ; => { {:keys [handler-fn handler-stats_ dispatch-opts]}} 106 | 107 | ;; Add console handler to print signals as human-readable text 108 | (tel/add-handler! :my-handler 109 | (tel/handler:console 110 | {:output-fn (tel/format-signal-fn {})})) 111 | 112 | ;; Add console handler to print signals as edn 113 | (tel/add-handler! :my-handler 114 | (tel/handler:console 115 | {:output-fn (tel/pr-signal-fn {:pr-fn :edn})})) 116 | 117 | ;; Add console handler to print signals as JSON 118 | ;; Ref. (or any alt JSON lib) 119 | #?(:clj (require '[jsonista.core :as jsonista])) 120 | (tel/add-handler! :my-handler 121 | (tel/handler:console 122 | {:output-fn 123 | #?(:cljs :json ; Use js/JSON.stringify 124 | :clj jsonista/write-value-as-string)})) 125 | 126 | ;;;; Docstring examples 127 | 128 | (tel/with-signal (tel/event! ::my-id)) 129 | (tel/with-signal (tel/event! ::my-id :warn)) 130 | (tel/with-signal 131 | (tel/event! ::my-id 132 | {:let [x "x"] ; Available to `:data` and `:msg` 133 | :data {:x x} 134 | :msg ["My msg:" x]})) 135 | 136 | (tel/with-signal (tel/log! "My msg")) 137 | (tel/with-signal (tel/log! :warn "My msg")) 138 | (tel/with-signal 139 | (tel/log! 140 | {:let [x "x"] ; Available to `:data` and `:msg` 141 | :data {:x x}} 142 | ["My msg:" x])) 143 | 144 | (tel/with-signal (throw (tel/error! (ex-info "MyEx" {})))) 145 | (tel/with-signal (throw (tel/error! ::my-id (ex-info "MyEx" {})))) 146 | (tel/with-signal 147 | (throw 148 | (tel/error! 149 | {:let [x "x"] ; Available to `:data` and `:msg` 150 | :data {:x x} 151 | :msg ["My msg:" x]} 152 | (ex-info "MyEx" {})))) 153 | 154 | (tel/with-signal (tel/trace! (+ 1 2))) 155 | (tel/with-signal (tel/trace! ::my-id (+ 1 2))) 156 | (tel/with-signal 157 | (tel/trace! 158 | {:let [x "x"] ; Available to `:data` and `:msg` 159 | :data {:x x}} 160 | (+ 1 2))) 161 | 162 | (tel/with-signal (tel/spy! (+ 1 2))) 163 | (tel/with-signal (tel/spy! :debug (+ 1 2))) 164 | (tel/with-signal 165 | (tel/spy! 166 | {:let [x "x"] ; Available to `:data` and `:msg` 167 | :data {:x x}} 168 | (+ 1 2))) 169 | 170 | (tel/with-signal (tel/catch->error! (/ 1 0))) 171 | (tel/with-signal (tel/catch->error! ::my-id (/ 1 0))) 172 | (tel/with-signal 173 | (tel/catch->error! 174 | {:let [x "x"] ; Available to `:data` and `:msg` 175 | :data {:x x} 176 | :msg ["My msg:" x] 177 | :catch-val "Return value when form throws"} 178 | (/ 1 0))) 179 | 180 | ;;;; Wiki examples 181 | 182 | ;;; Filter signals 183 | 184 | (tel/set-min-level! :info) ; Set global minimum level 185 | (tel/with-signal (tel/event! ::my-id1 :info)) ; => {:keys [inst id ...]} 186 | (tel/with-signal (tel/event! ::my-id1 :debug)) ; => nil (signal not allowed) 187 | 188 | (tel/with-min-level :trace ; Override global minimum level 189 | (tel/with-signal (tel/event! ::my-id1 :debug))) ; => {:keys [inst id ...]} 190 | 191 | ;; Disallow all signals in matching namespaces 192 | (tel/set-ns-filter! {:disallow "some.nosy.namespace.*"}) 193 | 194 | ;;; Configuring handlers 195 | 196 | ;; Create a test signal 197 | (def my-signal 198 | (tel/with-signal 199 | (tel/log! {:id ::my-id, :data {:x1 :x2}} "My message"))) 200 | 201 | ;; Create console handler with default opts (writes formatted string) 202 | (def my-handler (tel/handler:console {})) 203 | 204 | ;; Test handler, remember it's just a (fn [signal]) 205 | (my-handler my-signal) ; %> 206 | ;; 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My message 207 | ;; data: {:x1 :x2} 208 | 209 | ;; Create console handler which writes signals as edn 210 | (def my-handler 211 | (tel/handler:console 212 | {:output-fn (tel/pr-signal-fn {:pr-fn :edn})})) 213 | 214 | (my-handler my-signal) ; %> 215 | ;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...} 216 | 217 | ;; Create console handler which writes signals as JSON 218 | ;; Ref. (or any alt JSON lib) 219 | #?(:clj (require '[jsonista.core :as jsonista])) 220 | (def my-handler 221 | (tel/handler:console 222 | {:output-fn 223 | (tel/pr-signal-fn 224 | {:pr-fn 225 | #?(:cljs :json ; Use js/JSON.stringify 226 | :clj jsonista/write-value-as-string)})})) 227 | 228 | (my-handler my-signal) ; %> 229 | ;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...} 230 | 231 | ;; Deregister the default console handler 232 | (tel/remove-handler! :defaultel/console) 233 | 234 | ;; Register our custom console handler 235 | (tel/add-handler! :my-handler my-handler 236 | ;; Lots of options here for filtering, etc. 237 | ;; See `help:handler-dispatch-options` docstring! 238 | {}) 239 | 240 | ;; NB make sure to always stop handlers at the end of your 241 | ;; `-main` or shutdown procedure 242 | (tel/call-on-shutdown! 243 | (fn [] (tel/stop-handlers!))) 244 | 245 | ;; See `tel/help:handlers` docstring for more 246 | 247 | ;;; Writing handlers 248 | 249 | ;; Handlers are just fns of 2 arities 250 | 251 | (defn my-basic-handler 252 | ([]) ; Arity-0 called when stopping the handler 253 | ([signal] (println signal)) ; Arity-1 called when handling a signal 254 | ) 255 | 256 | ;; If you're making a customizable handler for use by others, it's often 257 | ;; handy to define a handler constructor 258 | 259 | (defn handler:my-fancy-handler ; Note constructor naming convention 260 | "Needs `some-lib`, Ref. . 261 | 262 | Returns a signal handler that: 263 | - Takes a Telemere signal (map). 264 | - Does something useful with the signal! 265 | 266 | Options: 267 | `:option1` - Option description 268 | `:option2` - Option description 269 | 270 | Tips: 271 | - Tip 1 272 | - Tip 2" 273 | 274 | ([] (handler:my-fancy-handler nil)) ; Use default opts (iff defaults viable) 275 | ([{:as constructor-opts}] 276 | 277 | ;; Do option validation and other prep here, i.e. try to keep 278 | ;; expensive work outside handler function when possible! 279 | 280 | (let [handler-fn ; Fn of exactly 2 arities (1 and 0) 281 | (fn a-handler:my-fancy-handler ; Note fn naming convention 282 | 283 | ([signal] ; Arity-1 called when handling a signal 284 | ;; Do something useful with the given signal (write to 285 | ;; console/file/queue/db, etc.). Return value is ignored. 286 | ) 287 | 288 | ([] ; Arity-0 called when stopping the handler 289 | ;; Flush buffers, close files, etc. May just noop. 290 | ;; Return value is ignored. 291 | ))] 292 | 293 | ;; (Advanced, optional) You can use metadata to provide default 294 | ;; handler dispatch options (see `help:handler-dispatch-options`) 295 | 296 | (with-meta handler-fn 297 | {:dispatch-opts 298 | {:min-level :info 299 | :limit 300 | [[1 1000] ; Max 1 signal per second 301 | [10 60000] ; Max 10 signals per minute 302 | ]}})))) 303 | 304 | ;;; Message building 305 | 306 | ;; A fixed message (string arg) 307 | (tel/log! "A fixed message") ; %> {:msg "A fixed message"} 308 | 309 | ;; A joined message (vector arg) 310 | (let [user-arg "Bob"] 311 | (tel/log! ["User" (str "`" user-arg "`") "just logged in!"])) 312 | ;; %> {:msg_ "User `Bob` just logged in!` ...} 313 | 314 | ;; With arg prep 315 | (let [user-arg "Bob" 316 | usd-balance-str "22.4821"] 317 | 318 | (tel/log! 319 | {:let 320 | [username (clojure.string/upper-case user-arg) 321 | usd-balance (parse-double usd-balance-str)] 322 | 323 | :data 324 | {:username username 325 | :usd-balance usd-balance}} 326 | 327 | ["User" username "has balance:" (str "$" (Math/round usd-balance))])) 328 | 329 | ;; %> {:msg "User BOB has balance: $22" ...} 330 | 331 | (tel/log! (str "This message " "was built " "by `str`")) 332 | ;; %> {:msg "This message was built by `str`"} 333 | 334 | (tel/log! (enc/format "This message was built by `%s`" "format")) 335 | ;; %> {:msg "This message was built by `format`"} 336 | 337 | ;;; App-level kvs 338 | 339 | (tel/with-signal 340 | (tel/event! ::my-id 341 | {:my-data-for-xfn "foo" 342 | :my-data-for-handler "bar"})) 343 | 344 | ;; %> 345 | ;; {;; App-level kvs included inline (assoc'd to signal root) 346 | ;; :my-data-for-xfn "foo" 347 | ;; :my-data-for-handler "bar" 348 | ;; :kvs ; And also collected together under ":kvs" key 349 | ;; {:my-data-for-xfn "foo" 350 | ;; :my-data-for-handler "bar"} 351 | ;; ... } 352 | 353 | ;;;; Misc extra examples 354 | 355 | (tel/log! {:id ::my-id, :data {:x1 :x2}} ["My 2-part" "message"]) ; %> 356 | ;; 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My 2-part message 357 | ;; data: {:x1 :x2} 358 | 359 | ;; `:let` bindings are available to `:data` and message, but only paid 360 | ;; for when allowed by minimum level and other filtering criteria 361 | (tel/log! 362 | {:level :info 363 | :let [expensive (reduce * (range 1 12))] ; 12 factorial 364 | :data {:my-metric expensive}} 365 | ["Message with metric:" expensive]) 366 | 367 | ;; With sampling 50% and 1/sec rate limiting 368 | (tel/log! 369 | {:sample 0.5 370 | :limit {"1 per sec" [1 1000]}} 371 | "This signal will be sampled and rate limited") 372 | 373 | ;; Several signal creators are available for convenience. 374 | ;; All offer the same options, but each has an API optimized 375 | ;; for a particular use case: 376 | 377 | (tel/log! {:level :info, :id ::my-id} "Hi!") ; [msg] or [level-or-opts msg] 378 | (tel/event! ::my-id {:level :info, :msg "Hi!"}) ; [id] or [id level-or-opts] 379 | (tel/signal! {:level :info, :id ::my-id, :msg "Hi!"}) ; [opts] 380 | 381 | ) 382 | -------------------------------------------------------------------------------- /handlers/consoles.cljc: -------------------------------------------------------------------------------- 1 | ../main/src/taoensso/telemere/consoles.cljc -------------------------------------------------------------------------------- /handlers/files.clj: -------------------------------------------------------------------------------- 1 | ../main/src/taoensso/telemere/files.clj -------------------------------------------------------------------------------- /handlers/open_telemetry.clj: -------------------------------------------------------------------------------- 1 | ../main/src/taoensso/telemere/open_telemetry.clj -------------------------------------------------------------------------------- /handlers/postal.clj: -------------------------------------------------------------------------------- 1 | ../main/src/taoensso/telemere/postal.clj -------------------------------------------------------------------------------- /handlers/slack.clj: -------------------------------------------------------------------------------- 1 | ../main/src/taoensso/telemere/slack.clj -------------------------------------------------------------------------------- /handlers/sockets.clj: -------------------------------------------------------------------------------- 1 | ../main/src/taoensso/telemere/sockets.clj -------------------------------------------------------------------------------- /imgs/handler-output-clj-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoensso/telemere/b4357435f6410036e5cec0d04baffe2acec098cc/imgs/handler-output-clj-file.png -------------------------------------------------------------------------------- /imgs/handler-output-cljs-console-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoensso/telemere/b4357435f6410036e5cec0d04baffe2acec098cc/imgs/handler-output-cljs-console-raw.png -------------------------------------------------------------------------------- /imgs/handler-output-cljs-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoensso/telemere/b4357435f6410036e5cec0d04baffe2acec098cc/imgs/handler-output-cljs-console.png -------------------------------------------------------------------------------- /imgs/telemere-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /imgs/telemere.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoensso/telemere/b4357435f6410036e5cec0d04baffe2acec098cc/imgs/telemere.fig -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd main; lein install; cd ..; 4 | cd slf4j; lein install; cd ..; 5 | 6 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /main/project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.taoensso/telemere "1.1.0" 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.153.1"]] 14 | 15 | :test-paths ["test" #_"src"] 16 | 17 | :profiles 18 | {;; :default [:base :system :user :provided :dev] 19 | :provided {:dependencies [[org.clojure/clojurescript "1.12.42"] 20 | [org.clojure/clojure "1.11.4"]]} 21 | :c1.12 {:dependencies [[org.clojure/clojure "1.12.1"]]} 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.1"] 51 | [org.clojure/tools.logging "1.3.0"] 52 | [org.slf4j/slf4j-api "2.0.17"] 53 | [com.taoensso/telemere-slf4j "1.1.0"] 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.53.0"] 60 | [io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.53.0"] 61 | [io.opentelemetry/opentelemetry-exporter-otlp "1.53.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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), before establishing `:let` ?binding 17 | `:let` --------- ?bindings to establish conditionally (iff signal allowed), 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. 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main/shadow-cljs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npx shadow-cljs watch main 3 | -------------------------------------------------------------------------------- /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/files.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc taoensso.telemere.files 2 | "Telemere -> file handler." 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 | ;;;; Implementation 14 | 15 | (defn gzip-file 16 | "Compresses contents of `file-in` to `file-out` using gzip." 17 | [file-in file-out] 18 | (let [file-in (utils/as-file file-in) 19 | file-out (utils/as-file file-out)] 20 | 21 | (with-open 22 | [stream-in (java.io.FileInputStream. file-in) 23 | stream-out (java.io.FileOutputStream. file-out) 24 | gz-out (java.util.zip.GZIPOutputStream. stream-out 2048 false)] 25 | 26 | (let [read-buffer (byte-array (.length file-in))] 27 | (loop [] 28 | (let [bytes-read (.read stream-in read-buffer)] 29 | (when-not (== -1 bytes-read) 30 | (.write gz-out read-buffer 0 bytes-read)))))) 31 | 32 | true)) 33 | 34 | (comment (gzip-file "foo.txt" "foo.txt.gz")) 35 | 36 | (defn get-file-name 37 | "(main-path)(-YYYY-MM-DD(d/w/m))(.part)?(.gz)?" 38 | ^String [main-path ?timestamp ?part gz?] 39 | (str main-path 40 | (when-let [ts ?timestamp] (str "-" ts)) 41 | (when-let [p ?part] (str "." p (when gz? ".gz"))))) 42 | 43 | (comment (get-file-name "test/logs/app.log" nil nil true)) 44 | 45 | ;; Timestamp handling, edy (long epoch day) as base type 46 | (let [utc java.time.ZoneOffset/UTC 47 | ^java.time.format.DateTimeFormatter dtf 48 | (.withZone java.time.format.DateTimeFormatter/ISO_LOCAL_DATE 49 | utc)] 50 | 51 | (let [cf (* 24 60 60 1000)] 52 | (defn udt->edy ^long [^long udt] (quot udt cf)) 53 | (defn edy->udt ^long [^long edy] (* edy cf))) 54 | 55 | (let [ta (java.time.temporal.TemporalAdjusters/previousOrSame java.time.DayOfWeek/MONDAY)] 56 | (defn edy-week ^long [^long edy] (.toEpochDay (.with (java.time.LocalDate/ofEpochDay edy) ta)))) 57 | 58 | (let [ta (java.time.temporal.TemporalAdjusters/firstDayOfMonth)] 59 | (defn edy-month ^long [^long edy] (.toEpochDay (.with (java.time.LocalDate/ofEpochDay edy) ta)))) 60 | 61 | (defn file-timestamp->edy ^long [^String timestamp] 62 | (let [timestamp (subs timestamp 0 (dec (count timestamp)))] 63 | (.toEpochDay (java.time.LocalDate/parse timestamp dtf)))) 64 | 65 | (defn file-last-modified->edy ^long [^java.io.File file] 66 | (.toEpochDay (.toLocalDate (.atZone (java.time.Instant/ofEpochMilli (.lastModified file)) utc)))) 67 | 68 | (defn format-file-timestamp 69 | ^String [interval ^long edy] 70 | (case interval 71 | :daily (str (.format dtf (java.time.LocalDate/ofEpochDay edy)) "d") 72 | :weekly (str (.format dtf (java.time.LocalDate/ofEpochDay (edy-week edy))) "w") 73 | :monthly (str (.format dtf (java.time.LocalDate/ofEpochDay (edy-month edy))) "m") 74 | (truss/unexpected-arg! interval 75 | {:param 'interval 76 | :context `file-timestamp 77 | :expected #{:daily :weekly :monthly}})))) 78 | 79 | (comment (file-timestamp->edy (format-file-timestamp :weekly (udt->edy (enc/now-udt*))))) 80 | 81 | (defn manage-test-files! 82 | "Describes/creates/deletes files used for tests/debugging, etc." 83 | [action] 84 | (truss/have? [:el #{:return :println :create :delete}] action) 85 | (let [fnames_ (volatile! []) 86 | action! 87 | (fn [app timestamp part gz? timestamp main?] 88 | (let [path (str "test/logs/app" app ".log") 89 | fname (get-file-name path (when-not main? timestamp) part gz?) 90 | file (utils/as-file fname)] 91 | 92 | (case action 93 | :return nil 94 | :println (println fname) 95 | :delete (.delete file) 96 | :create 97 | (do 98 | (utils/writeable-file! file) 99 | (spit file fname) 100 | (when timestamp 101 | (.setLastModified file 102 | (edy->udt (file-timestamp->edy timestamp)))))) 103 | 104 | (vswap! fnames_ conj fname)))] 105 | 106 | (doseq [{:keys [app gz? timestamps parts]} 107 | [{:app 1} 108 | {:app 2, :gz? true, :parts [1 2 3 4 5]} 109 | {:app 3, :gz? false, :parts [1 2 3 4 5]} 110 | 111 | {:app 4, :gz? true, :parts [1 2 3 4 5]} 112 | {:app 4, :gz? false, :parts [1 2 3 4 5]} 113 | 114 | {:app 5, :gz? true, :timestamps 115 | ["2020-01-01d" "2020-01-02d" "2020-02-01d" "2020-02-02d" "2021-01-01d" 116 | "2020-01-01w" "2020-02-01m"]} 117 | 118 | {:app 6, :gz? true, :parts [1 2 3 4 5], 119 | :timestamps 120 | ["2020-01-01d" "2020-01-02d" "2020-02-01d" "2020-02-02d" "2021-01-01d" 121 | "2020-01-01w" "2020-02-01m"]}]] 122 | 123 | (action! app nil nil false (peek timestamps) :main) 124 | 125 | (doseq [timestamp (or timestamps [nil]) 126 | part (or parts [nil])] 127 | 128 | (action! app timestamp part gz? timestamp (not :main)))) 129 | 130 | @fnames_)) 131 | 132 | (comment (manage-test-files! :create)) 133 | 134 | (defn scan-files 135 | "Returns ?[{:keys [file edy part ...]}] for files in same dir as `main-path` that: 136 | - Have the same `interval` type ∈ #{:daily :weekly :monthly nil} (=> ?timestamped). 137 | - Have the given timestamp (e.g. \"2020-01-01d\", or nil for NO timestamp)." 138 | [main-path interval timestamp sort?] 139 | (truss/have? [:el #{:daily :weekly :monthly nil}] interval) 140 | (let [main-file (utils/as-file main-path) ; `logs/app.log` 141 | main-dir (.getParentFile (.getAbsoluteFile main-file)) ; `.../logs` 142 | 143 | file-pattern ; Matches ?[_ timestamp part gz] 144 | (let [main (str "\\Q" (.getName main-file) "\\E") 145 | end "(\\.\\d+)?(\\.gz)?"] 146 | 147 | (if interval 148 | (let [ts-suffix (case interval :daily "d" :weekly "w" :monthly "m")] 149 | (re-pattern (str main "-(\\d{4}-\\d{2}-\\d{2}" ts-suffix ")" end))) 150 | (re-pattern (str main "(__no-timestamp__)?" end)))) 151 | 152 | ref-timestamp timestamp 153 | any-timestamp? (and interval (nil? ref-timestamp))] 154 | 155 | (when-let [file-maps 156 | (not-empty 157 | (reduce 158 | (fn [acc ^java.io.File file-in] 159 | (or 160 | (when-let [[_ timestamp part gz] (re-matches file-pattern (.getName file-in))] 161 | (when (or any-timestamp? (= timestamp ref-timestamp)) 162 | (let [edy (when timestamp (file-timestamp->edy timestamp)) 163 | part (when part (enc/as-pos-int (subs part 1))) 164 | gz? (boolean gz) 165 | file-name (get-file-name main-path timestamp part gz?)] 166 | 167 | ;; Verify that scanned file name matches our template 168 | (let [actual (.getAbsolutePath file-in) 169 | expected file-name] 170 | (when-not (.endsWith actual expected) 171 | (truss/ex-info! "Unexpected file name" 172 | {:actual actual, :expected expected}))) 173 | 174 | (conj acc 175 | {:file file-in 176 | :file-name file-name 177 | :timestamp timestamp 178 | :edy edy 179 | :part part 180 | :gz? gz?})))) 181 | acc)) 182 | [] (.listFiles main-dir)))] 183 | 184 | (if sort? ; For unit tests, etc. 185 | (sort-by (fn [{:keys [edy part]}] [edy part]) file-maps) 186 | (do file-maps))))) 187 | 188 | (comment (group-by :edy (scan-files "logs/app.log" nil nil false))) 189 | (comment 190 | (mapv #(select-keys % [:full-name :edy :part :gz?]) 191 | (scan-files "test/logs/app6.log" :daily nil :sort))) 192 | 193 | ;; Debugger used to test/debug file ops 194 | (defn debugger [] (let [log_ (volatile! [])] (fn ([ ] @log_) ([x] (vswap! log_ conj x))))) 195 | 196 | (defn archive-main-file! 197 | "Renames main -> .1.gz archive. Makes room by first rotating 198 | pre-existing parts (n->n+1) and maintaining `max-num-parts` limit. 199 | Expensive. Must manually reset any main file streams after!" 200 | [main-path interval timestamp max-num-parts gz? ?debugger] 201 | 202 | ;; Rename n->n+1, deleting when n+1>max 203 | (when-let [file-maps (scan-files main-path interval timestamp false)] ; [ ...] 204 | (let [file-maps-by-edy (group-by :edy file-maps)] ; { [ ...]} 205 | (enc/run-kv! 206 | (fn [edy file-maps] 207 | (doseq [{:keys [^java.io.File file file-name timestamp part gz?]} 208 | (sort-by :part enc/rcompare file-maps)] 209 | 210 | (when part 211 | (let [part (long part) 212 | part+ (inc part)] 213 | 214 | (if-let [drop? (and max-num-parts (> part+ (long max-num-parts)))] 215 | (if-let [df ?debugger] 216 | (df [:delete file-name]) 217 | (.delete file)) 218 | 219 | (let [file-name+ (get-file-name main-path timestamp part+ gz?)] 220 | (if-let [df ?debugger] 221 | (df [:rename file-name file-name+]) 222 | (.renameTo file (utils/as-file file-name+))))))))) 223 | file-maps-by-edy))) 224 | 225 | ;; Rename main -> .1.gz archive 226 | (let [arch-file-name-gz (get-file-name main-path timestamp 1 false) 227 | arch-file-name+gz (get-file-name main-path timestamp 1 gz?)] 228 | 229 | (if-let [df ?debugger] 230 | (df [:rename main-path arch-file-name+gz]) 231 | (let [main-file (utils/as-file main-path) ; `logs/app.log` 232 | arch-file-gz (utils/as-file arch-file-name-gz) ; `logs/app.log.1` or `logs/app.log-2020-01-01d.1` 233 | arch-file+gz (utils/as-file arch-file-name+gz) ; `logs/app.log.1.gz` or `logs/app.log-2020-01-01d.1.gz` 234 | ] 235 | 236 | (truss/have? false? (.exists arch-file+gz)) ; No pre-existing `.1.gz` 237 | (.renameTo main-file arch-file-gz) 238 | (.createNewFile main-file) 239 | 240 | (when gz? 241 | (gzip-file arch-file-gz arch-file+gz) 242 | (.delete arch-file-gz)))))) 243 | 244 | (defn prune-archive-files! 245 | "Scans files in same dir as `main-path`, and maintains `max-num-intervals` limit 246 | by deleting ALL parts for oldest intervals. Expensive." 247 | [main-path interval max-num-intervals ?debugger] 248 | (when (and interval max-num-intervals) 249 | (when-let [file-maps (scan-files main-path interval nil false)] ; [ ...] 250 | (let [file-maps-by-edy (group-by :edy file-maps) ; { [ ...]} 251 | n-prune (- (count file-maps-by-edy) (long max-num-intervals))] 252 | 253 | (when (pos? n-prune) ; Prune some (oldest) intervals 254 | (doseq [old-edy (take n-prune (sort (keys file-maps-by-edy)))] 255 | 256 | ;; Delete every part of this interval 257 | (doseq [{:keys [^java.io.File file file-name]} 258 | (sort-by :part enc/rcompare 259 | (get file-maps-by-edy old-edy))] 260 | 261 | (if-let [df ?debugger] 262 | (df [:delete file-name]) 263 | (.delete file))))))))) 264 | 265 | ;;;; Handler 266 | 267 | (defn ^:public handler:file 268 | "Experimental, subject to change. 269 | 270 | Returns a signal handler that: 271 | - Takes a Telemere signal (map). 272 | - Writes (appends) the signal as a string to file specified by `path`. 273 | 274 | Can output signals as human or machine-readable (edn, JSON) strings. 275 | 276 | Depending on options, archive file/s may also be maintained: 277 | - `logs/app.log.n.gz` (for nil `:interval`, non-nil `:max-file-size`) 278 | - `logs/app.log-YYYY-MM-DDd.n.gz` (for non-nil `:interval`) ; d=daily/w=weekly/m=monthly 279 | 280 | Example files with default options: 281 | `/logs/telemere.log` ; Current file (newest entries) 282 | `/logs/telemere.log-2020-01-01m.1.gz` ; Archive for Jan 2020, part 1 (newest entries) 283 | ... 284 | `/logs/telemere.log-2020-01-01m.8.gz` ; Archive for Jan 2020, part 8 (oldest entries) 285 | 286 | Options: 287 | `:output-fn`- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn` 288 | `:path` ----- Path string of the target output file (default `logs/telemere.log`) 289 | 290 | `:interval` - ∈ #{nil :daily :weekly :monthly} (default `:monthly`) 291 | When non-nil, causes interval-based archives to be maintained. 292 | 293 | `:max-file-size` - ∈ #{nil } (default 4MB) 294 | When `path` file size > ~this many bytes, rotates old content to numbered archives. 295 | 296 | `:max-num-parts` - ∈ #{nil } (default 8) 297 | Maximum number of numbered archives to retain for any particular interval. 298 | 299 | `:max-num-intervals` - ∈ #{nil } (default 6) 300 | Maximum number of intervals (days/weeks/months) to retain." 301 | 302 | ([] (handler:file nil)) 303 | ([{:keys 304 | [output-fn 305 | path interval 306 | max-file-size 307 | max-num-parts 308 | max-num-intervals 309 | gzip-archives?] 310 | 311 | :or 312 | {output-fn (utils/format-signal-fn) 313 | path "logs/telemere.log" ; Main path, we'll ALWAYS write to this exact file 314 | interval :monthly 315 | max-file-size (* 1024 1024 4) ; 4MB 316 | max-num-parts 8 317 | max-num-intervals 6 318 | gzip-archives? true}}] 319 | 320 | (let [main-path path 321 | main-file (utils/as-file main-path) 322 | fw (utils/file-writer {:file main-file, :append? true}) 323 | 324 | >max-file-size? 325 | (when max-file-size 326 | (let [max-file-size (long max-file-size) 327 | rl (enc/rate-limiter-once-per 250)] 328 | (fn [] (and (not (rl)) (> (.length main-file) max-file-size))))) 329 | 330 | prev-timestamp_ (enc/latom nil) ; Initially nil 331 | curr-timestamp_ (enc/latom nil) ; Will be bootstrapped based on main file 332 | 333 | ;; Called on every write attempt, 334 | ;; maintains `timestamp_`s and returns true iff timestamp changed. 335 | new-interval!? 336 | (when interval 337 | (let [init-edy (let [n (file-last-modified->edy main-file)] (when (pos? n) n)) 338 | curr-edy_ (enc/latom init-edy) 339 | updated!? ; Returns ?[old new] on change 340 | (fn [latom_ new] 341 | (let [old (latom_)] 342 | (when 343 | (and 344 | (not= old new) 345 | (compare-and-set! latom_ old new)) 346 | [old new])))] 347 | 348 | (when init-edy ; Don't bootstrap "1970-01-01d", etc. 349 | (reset! curr-timestamp_ 350 | (format-file-timestamp interval init-edy))) 351 | 352 | (fn new-interval!? [] 353 | (let [curr-edy (udt->edy (System/currentTimeMillis))] 354 | (when (updated!? curr-edy_ curr-edy) ; Day changed 355 | (let [curr-timestamp (format-file-timestamp interval curr-edy)] 356 | (when-let [[prev-timestamp _] (updated!? curr-timestamp_ curr-timestamp)] 357 | ;; Timestamp changed (recall: interval may not be daily) 358 | (reset! prev-timestamp_ prev-timestamp) 359 | true))))))) 360 | 361 | lock (Object.)] 362 | 363 | (fn a-handler:file 364 | ([ ] (locking lock (fw))) ; Stop => close writer 365 | ([signal] 366 | (when-let [output (output-fn signal)] 367 | (let [new-interval? (when interval (new-interval!?)) 368 | >max-file-size? (when max-file-size (>max-file-size?)) 369 | reset-stream? (or new-interval? >max-file-size?)] 370 | 371 | (locking lock 372 | 373 | (if new-interval? 374 | (do 375 | ;; Rename main -> .1.gz, etc. 376 | (when-let [prev-timestamp (prev-timestamp_)] 377 | (archive-main-file! main-path interval prev-timestamp 378 | max-num-parts gzip-archives? nil)) 379 | 380 | (when max-num-intervals 381 | (prune-archive-files! main-path interval 382 | max-num-intervals nil))) 383 | 384 | (when >max-file-size? 385 | ;; Rename main -> .1.gz, etc. 386 | (archive-main-file! main-path interval (curr-timestamp_) 387 | max-num-parts gzip-archives? nil))) 388 | 389 | (when reset-stream? (fw :writer/reset!)) 390 | (do (fw output)))))))))) 391 | 392 | (comment 393 | (manage-test-files! :create) 394 | (.setLastModified (utils/as-file "test/logs/app6.log") 395 | (enc/as-udt "1999-01-01T01:00:00.00Z")) 396 | 397 | (let [f (utils/as-file "test/logs/app6.log")] (enc/qb 1e5 (.length f))) 398 | (let [hfn 399 | (handler:file 400 | {:path "test/logs/app6.log" 401 | :max-num-intervals 2 402 | :max-num-parts 2})] 403 | 404 | (hfn {:info :level :msg_ "hello"}) (hfn))) 405 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /main/src/taoensso/telemere/timbre.cljc: -------------------------------------------------------------------------------- 1 | (ns taoensso.telemere.timbre 2 | "Main Timbre macros, reimplemented on top of Telemere. 3 | Intended to help ease migration from Timbre to Telemere." 4 | (:require 5 | [clojure.string :as str] 6 | [taoensso.truss :as truss] 7 | [taoensso.encore :as enc] 8 | [taoensso.telemere.impl :as impl] 9 | [taoensso.telemere :as tel])) 10 | 11 | (comment 12 | (remove-ns (symbol (str *ns*))) 13 | (:api (enc/interns-overview))) 14 | 15 | (let [arg-str 16 | (fn [x] 17 | (enc/cond 18 | (nil? x) "nil" 19 | (record? x) (pr-str x) 20 | :else x))] 21 | 22 | (defn ^:no-doc parse-vargs 23 | "Private, don't use. Adapted from Timbre." 24 | [format-msg? vargs] 25 | (let [[v0] vargs] 26 | 27 | (if (truss/error? v0) 28 | (let [error v0 29 | vargs (enc/vrest vargs) 30 | pattern (if format-msg? (let [[v0] vargs] v0) nil) 31 | vargs (if format-msg? (enc/vrest vargs) vargs) 32 | msg 33 | (delay 34 | (if format-msg? 35 | (enc/format* pattern vargs) 36 | (enc/str-join " " (map arg-str) vargs)))] 37 | 38 | [error msg {:vargs vargs}]) 39 | 40 | (let [md (if (and (map? v0) (get (meta v0) :meta)) v0 nil) 41 | error (get md :err) 42 | md (dissoc md :err) 43 | vargs (if md (enc/vrest vargs) vargs) 44 | pattern (if format-msg? (let [[v0] vargs] v0) nil) 45 | vargs (if format-msg? (enc/vrest vargs) vargs) 46 | msg 47 | (delay 48 | (if format-msg? 49 | (enc/format* pattern vargs) 50 | (enc/str-join " " (map arg-str) vargs)))] 51 | 52 | [error msg (when-not (empty? vargs) {:vargs vargs})]))))) 53 | 54 | (comment 55 | (parse-vargs true [ "hello %s" "stu"]) 56 | (parse-vargs true [(Exception. "Ex1") "hello %s" "stu"])) 57 | 58 | (def ^:no-doc ^:const shim-id :taoensso.telemere/timbre) 59 | 60 | #?(:clj 61 | (defmacro ^:no-doc log! 62 | "Private, don't use." 63 | [level format-msg? vargs] 64 | (truss/keep-callsite 65 | `(when (impl/signal-allowed? {:kind :log, :level ~level, :id shim-id}) 66 | (let [[error# msg# data#] (parse-vargs ~format-msg? ~vargs)] 67 | (tel/log! 68 | {:allow? true 69 | :level ~level 70 | :id shim-id 71 | :error error# 72 | :data data#} 73 | msg#) 74 | nil))))) 75 | 76 | (comment 77 | (macroexpand '(trace "foo")) 78 | (tel/with-signal (trace "foo")) 79 | (tel/with-signal (infof "Hello %s" "world"))) 80 | 81 | #?(:clj 82 | (do 83 | (defmacro log "Prefer `telemere/log!`, etc." [level & args] (truss/keep-callsite `(log! ~level false [~@args]))) 84 | (defmacro trace "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :trace false [~@args]))) 85 | (defmacro debug "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :debug false [~@args]))) 86 | (defmacro info "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :info false [~@args]))) 87 | (defmacro warn "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :warn false [~@args]))) 88 | (defmacro error "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :error false [~@args]))) 89 | (defmacro fatal "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :fatal false [~@args]))) 90 | (defmacro report "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :report false [~@args]))) 91 | 92 | (defmacro logf "Prefer `telemere/log!`, etc." [level & args] (truss/keep-callsite `(log! ~level true [~@args]))) 93 | (defmacro tracef "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :trace true [~@args]))) 94 | (defmacro debugf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :debug true [~@args]))) 95 | (defmacro infof "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :info true [~@args]))) 96 | (defmacro warnf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :warn true [~@args]))) 97 | (defmacro errorf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :error true [~@args]))) 98 | (defmacro fatalf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :fatal true [~@args]))) 99 | (defmacro reportf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :report true [~@args]))))) 100 | 101 | #?(:clj 102 | (defmacro spy 103 | "Prefer `telemere/spy!`. 104 | 105 | Note that for extra flexibility and improved interop with Open Telemetry, 106 | this shim intentionally handles errors (forms that throw) slightly differently 107 | to Timbre's original `spy`: 108 | 109 | When the given `form` throws, this shim may create an ADDITIONAL signal of 110 | `:error` kind and level. The behaviour is equivalent to: 111 | 112 | (telemere/spy! level ; Creates 0/1 `:spy` signals with given/default (`:debug`) level 113 | (telemere/catch->error! form)) ; Creates 0/1 `:error` signals with `:error` level 114 | 115 | The additional signal helps to separate the success and error cases for 116 | individual filtering and/or handling." 117 | 118 | ([ form] (truss/keep-callsite `(spy :debug nil ~form))) 119 | ([level form] (truss/keep-callsite `(spy ~level nil ~form))) 120 | ([level form-name form] 121 | (let [ns (str *ns*) 122 | coords (truss/callsite-coords &form) 123 | msg 124 | (if form-name 125 | `(fn [_form# value# error# nsecs#] (impl/default-trace-msg ~form-name value# error# nsecs#)) 126 | `(fn [_form# value# error# nsecs#] (impl/default-trace-msg '~form value# error# nsecs#)))] 127 | 128 | `(tel/spy! 129 | {:ns ~ns 130 | :coords ~coords 131 | :id shim-id 132 | :level ~level 133 | :msg ~msg} 134 | 135 | (tel/catch->error! 136 | {:ns ~ns 137 | :coords ~coords 138 | :id shim-id} 139 | ~form)))))) 140 | 141 | (comment 142 | (:level (tel/with-signal (spy (/ 1 0)))) 143 | (select-keys (tel/with-signal (spy :info #_"my-form-name" (+ 1 2))) [:level :msg_]) 144 | (select-keys (tel/with-signal (spy :info #_"my-form-name" (throw (Exception. "Ex")))) [:level :msg_])) 145 | 146 | #?(:clj (defmacro log-errors "Prefer `telemere/catch->error!`." [& body] (truss/keep-callsite `(tel/catch->error! {:id shim-id, :catch-val nil} (do ~@body))))) 147 | #?(:clj (defmacro log-and-rethrow-errors "Prefer `telemere/catch->error!`." [& body] (truss/keep-callsite `(tel/catch->error! {:id shim-id} (do ~@body))))) 148 | #?(:clj (defmacro logged-future "Prefer `telemere/catch->error!`." [& body] (truss/keep-callsite `(future (tel/catch->error! {:id shim-id} (do ~@body)))))) 149 | 150 | #?(:clj 151 | (defmacro refer-timbre 152 | "(require 153 | '[taoensso.telemere.timbre :as timbre :refer 154 | [log trace debug info warn error fatal report 155 | logf tracef debugf infof warnf errorf fatalf reportf 156 | spy]])" 157 | [] 158 | `(require 159 | '~'[taoensso.telemere.timbre :as timbre :refer 160 | [log trace debug info warn error fatal report 161 | logf tracef debugf infof warnf errorf fatalf reportf 162 | spy]]))) 163 | 164 | ;;;; 165 | 166 | (defn set-min-level! "Prefer `telemere/set-min-level!`." [min-level] (tel/set-min-level! min-level)) 167 | #?(:clj 168 | (defmacro with-min-level 169 | "Prefer `telemere/with-min-level`." 170 | [min-level & body] 171 | `(tel/with-min-level ~min-level (do ~@body)))) 172 | 173 | #?(:clj 174 | (defmacro set-ns-min-level! 175 | "Prefer `telemere/set-min-level!`." 176 | ([ ?min-level] `(set-ns-min-level! ~(str *ns*) ~?min-level)) 177 | ([ns ?min-level] `(tel/set-min-level! nil ~(str ns) ~?min-level)))) 178 | 179 | #?(:clj (defmacro with-context "Prefer `telemere/with-ctx`." [context & body] `(tel/with-ctx ~context (do ~@body)))) 180 | #?(:clj (defmacro with-context+ "Prefer `telemere/with-ctx+`." [context & body] `(tel/with-ctx+ ~context (do ~@body)))) 181 | 182 | (defn shutdown-appenders! 183 | "Prefer `telemere/stop-handlers!`." 184 | [] (tel/stop-handlers!)) 185 | 186 | (defn timbre->telemere-appender 187 | "Returns a simple Timbre appender that'll redirect logs to Telemere." 188 | [] 189 | {:enabled? true 190 | :min-level nil 191 | :fn 192 | (fn [data] 193 | (let [{:keys [instant level context ?err output_ 194 | ?ns-str ?file ?line ?column]} data] 195 | 196 | (taoensso.telemere/signal! 197 | {:kind :timbre 198 | :level level 199 | :inst (taoensso.encore/as-?inst instant) 200 | :ctx+ context 201 | 202 | :ns ?ns-str 203 | :file ?file 204 | :line ?line 205 | :column ?column 206 | 207 | :error ?err 208 | :msg (force output_)})))}) 209 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /slf4j/project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.taoensso/telemere-slf4j "1.1.0" 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.1"] 20 | [org.slf4j/slf4j-api "2.0.17"] 21 | [com.taoensso/telemere "1.1.0"]]} 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 | -------------------------------------------------------------------------------- /slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider: -------------------------------------------------------------------------------- 1 | com.taoensso.telemere.slf4j.TelemereServiceProvider -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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 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 (marker-names in)))) 86 | acc (.iterator m)))) 87 | 88 | ;; Vector of markers 89 | (reduce 90 | (fn [acc in] (into acc (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 | (marker-names m1) 101 | (marker-names cm) 102 | (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 | :data 136 | (enc/assoc-some nil 137 | :slf4j/marker-names marker-names 138 | :slf4j/args (when args (vec args)) 139 | :slf4j/kvs kvs)}) 140 | nil) 141 | 142 | (defn- log! 143 | "Called by `com.taoensso.telemere.slf4j.TelemereLogger`." 144 | 145 | ;; Modern "fluent" API calls 146 | ([logger-name ^org.slf4j.event.LoggingEvent event] 147 | (let [inst (or (when-let [ts (.getTimeStamp event)] (java.time.Instant/ofEpochMilli ts)) (enc/now-inst*)) 148 | level (.getLevel event) 149 | error (.getThrowable event) 150 | msg-pattern (.getMessage event) 151 | args (when-let [args (.getArgumentArray event)] args) 152 | markers (when-let [markers (.getMarkers event)] (marker-names (vec markers))) 153 | kvs (when-let [kvps (.getKeyValuePairs event)] 154 | (reduce 155 | (fn [acc ^org.slf4j.event.KeyValuePair kvp] 156 | (assoc acc (.-key kvp) (.-value kvp))) 157 | nil kvps))] 158 | 159 | (when-debug (println [:slf4j/fluent-log-call (sig-level level) logger-name])) 160 | (normalized-log! logger-name level inst error msg-pattern args markers kvs))) 161 | 162 | ;; Legacy API calls 163 | ([logger-name ^org.slf4j.event.Level level error msg-pattern args marker] 164 | (let [marker-names (when marker (marker-names marker))] 165 | (when-debug (println [:slf4j/legacy-log-call (sig-level level) logger-name])) 166 | (normalized-log! logger-name level (enc/now-inst*) error msg-pattern args marker-names nil)))) 167 | 168 | (comment 169 | (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "my.class")) 170 | (impl/with-signal (-> sl (.info "Hello {}" "x"))) 171 | (impl/with-signal (-> (.atInfo sl) (.log "Hello {}" "x"))) 172 | 173 | (do ; Will noop with `NOPMDCAdapter` 174 | (org.slf4j.MDC/put "key" "val") 175 | (org.slf4j.MDC/get "key") 176 | (org.slf4j.MDC/getCopyOfContextMap) 177 | (org.slf4j.MDC/clear))) 178 | 179 | ;;;; 180 | 181 | (defn check-interop 182 | "Returns interop debug info map." 183 | [] 184 | (let [^org.slf4j.Logger sl 185 | (org.slf4j.LoggerFactory/getLogger "InteropTestTelemereLogger") 186 | sending? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl) 187 | receiving? 188 | (and sending? 189 | (impl/test-interop! "SLF4J -> Telemere" #(.info sl %)))] 190 | 191 | {:present? true 192 | :telemere-provider-present? true 193 | :sending->telemere? sending? 194 | :telemere-receiving? receiving?})) 195 | 196 | (impl/add-interop-check! :slf4j check-interop) 197 | 198 | (impl/on-init 199 | (impl/signal! 200 | {:kind :event 201 | :level :debug ; < :info since runs on init 202 | :id :taoensso.telemere/slf4j->telemere! 203 | :msg "Enabling interop: SLF4J -> Telemere"})) 204 | -------------------------------------------------------------------------------- /wiki/.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /wiki/1-Getting-started.md: -------------------------------------------------------------------------------- 1 | Telemere logo 2 | 3 | # Introduction 4 | 5 | Telemere is a **structured telemetry** library and next-generation replacement for [Timbre](https://www.taoensso.com/timbre). It helps enable the creation of Clojure/Script systems that are highly **observable**, **robust**, and **debuggable**. 6 | 7 | Its key function is to help: 8 | 9 | 1. **Capture data** in your running Clojure/Script programs, and 10 | 2. **Facilitate processing** of that data into **useful information / insight**. 11 | 12 | > [Terminology] *Telemetry* derives from the Greek *tele* (remote) and *metron* (measure). It refers to the collection of *in situ* (in position) data, for transmission to other systems for monitoring/analysis. *Logs* are the most common form of software telemetry. So think of telemetry as the *superset of logging-like activities* that help monitor and understand (software) systems. 13 | 14 | ## Signals 15 | 16 | The basic unit of data in Telemere is the **signal**. 17 | 18 | Signals include **traditional log messages**, **structured log messages**, and **events**. Telemere doesn't make a hard distinction between these - *they're all just signals* with [various attributes](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content). 19 | 20 | And they're represented by plain **Clojure/Script maps** with those attributes (keys). 21 | 22 | Fundamentally **all signals**: 23 | 24 | - Occur or are observed at a particular **location** in your code (namespace, line, column). 25 | - Occur or are observed *within* a particular **program state** / context. 26 | - Convey something of value *about* that **program state** / context. 27 | 28 | Signals may be *independently* valuable, valuable *in the aggregate* (e.g. statistically), or valuable *in association* with other related signals (e.g. while tracing the flow of some logical activity). 29 | 30 | ## Functionality 31 | 32 | The basic tools of Telemere are: 33 | 34 | 1. **Signal creators** to conditionally *create* signal maps at points in your code. 35 | 2. **Signal handlers** to conditionally *handle* those signal maps (analyse, write to 36 | console/file/queue/db, etc.). 37 | 38 | This is just a generalization of **traditional logging** which: 39 | 40 | - Conditionally creates **message strings** at points in your code. 41 | - Usually *dumps* those message strings somewhere for future parsing by human eyes or automated tools. 42 | 43 | ## Data types and structures 44 | 45 | The parsing of traditional log messages is often expensive, fragile, and lossy. So a key principle of **structured logging** is to **avoid parsing**, by instead **preserving data types and structures** whenever possible. 46 | 47 | Telemere embraces this principle by making such preservation *natural and convenient*. 48 | 49 | ## Noise reduction 50 | 51 | Not all data is equally valuable. 52 | 53 | 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. 54 | 55 | Telemere embraces this principle by making **effective filtering** likewise *natural and convenient*: 56 | 57 | Telemere sampling 58 | 59 | > Telemere uses the term **filtering** as the superset of both random sampling and other forms of data exclusion/reduction. 60 | 61 | ## Structured telemetry 62 | 63 | To conclude- Telemere handles **structured and traditional logging**, **tracing**, and **basic performance monitoring** with a simple unified API that: 64 | 65 | - Preserves data types and structures with **rich signals**, and 66 | - Offers effective noise reduction with **signal filtering**. 67 | 68 | Its name is a combination of _telemetry_ and _telomere_: 69 | 70 | > *Telemetry* derives from the Greek *tele* (remote) and *metron* (measure). It refers to the collection of *in situ* (in position) data, for transmission to other systems for monitoring/analysis. *Logs* are the most common form of software telemetry. So think of telemetry as the *superset of logging-like activities* that help monitor and understand (software) systems. 71 | 72 | > *Telomere* derives from the Greek *télos* (end) and *méros* (part). It refers to a genetic feature commonly found at the end of linear chromosomes that helps to protect chromosome integrity (think biological checksum). 73 | 74 | # Setup 75 | 76 | Add the [relevant dependency](../#latest-releases) to your project: 77 | 78 | ```clojure 79 | Leiningen: [com.taoensso/telemere "x-y-z"] ; or 80 | deps.edn: com.taoensso/telemere {:mvn/version "x-y-z"} 81 | ``` 82 | 83 | And setup your namespace imports: 84 | 85 | ```clojure 86 | (ns my-app (:require [taoensso.telemere :as tel])) 87 | ``` 88 | 89 | # Default config 90 | 91 | Telemere is configured sensibly out-the-box. 92 | See section [3-Config](./3-Config) for customization. 93 | 94 | **Default minimum level**: `:info` (signals with lower levels will noop). 95 | 96 | **Default signal handlers**: 97 | 98 | > Signal handlers process created signals to *do something with them* (analyse them, write them to console/file/queue/db, etc.) 99 | 100 | | Platform | Condition | Handler | 101 | | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | 102 | | Clj | Always | [Console handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) that prints signals to `*out*` or `*err*` | 103 | | Cljs | Always | [Console handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) that prints signals to the **browser console** | 104 | 105 | **Default interop**: 106 | 107 | > Telemere can create signals from relevant **external API calls**, etc. 108 | 109 | | Platform | Condition | Signals from | 110 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | 111 | | Clj | [SLF4J API](https://mvnrepository.com/artifact/org.slf4j/slf4j-api) and [Telemere SLF4J backend](https://clojars.org/com.taoensso/telemere-slf4j) present | [SLF4J](https://www.slf4j.org/) logging calls | 112 | | Clj | [tools.logging](https://mvnrepository.com/artifact/org.clojure/tools.logging) present and [`tools-logging->telemere!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.tools-logging#tools-logging-%3Etelemere!) called | [tools.logging](https://github.com/clojure/tools.logging) logging calls | 113 | | Clj | [`streams->telemere!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#streams-%3Etelemere!) called | Output to `System/out` and `System/err` streams | 114 | 115 | Interop can be tough to get configured correctly so the [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop) util is provided to help verify for tests or debugging: 116 | 117 | ```clojure 118 | (check-interop) ; => 119 | {:tools-logging {:present? false} 120 | :slf4j {:present? true, :telemere-receiving? true, ...} 121 | :open-telemetry {:present? true, :use-tracer? false, ...} 122 | :system/out {:telemere-receiving? false, ...} 123 | :system/err {:telemere-receiving? false, ...}} 124 | ``` 125 | 126 | # Usage 127 | 128 | ## Creating signals 129 | 130 | Telemere's signals are all created using the low-level `signal!` macro. You can use that directly, or one of the wrapper macros like `log!`. 131 | 132 | Several different wrapper macros are provided. The only difference between them: 133 | 134 | 1. They create signals with a different `:kind` value (which can be handy for filtering, etc.). 135 | 2. They have different positional arguments and/or return values optimised for concise calling in different use cases. 136 | 137 | **NB:** ALL wrapper macros can also just be called with a single [opts](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) map! 138 | 139 | See the linked docstrings below for more info: 140 | 141 | | Name | Args | Returns | 142 | | :---------------------------------------------------------------------------------------------------------- | :------------------------- | :--------------------------- | 143 | | [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) | `[opts]` or `[?level msg]` | nil | 144 | | [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) | `[opts]` or `[id ?level]` | nil | 145 | | [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!) | `[opts]` or `[?id run]` | Form result | 146 | | [`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) | `[opts]` or `[?level run]` | Form result | 147 | | [`error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error!) | `[opts]` or `[?id error]` | Given error | 148 | | [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) | `[opts]` or `[?id error]` | Form value or given fallback | 149 | | [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) | `[opts]` | Depends on opts | 150 | 151 | ## Checking signals 152 | 153 | Use the [`with-signal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signal) or (advanced) [`with-signals`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signals) utils to help test/debug the signals that you're creating: 154 | 155 | ```clojure 156 | (tel/with-signal 157 | (tel/log! 158 | {:let [x "x"] 159 | :data {:x x}} 160 | ["My msg:" x])) 161 | 162 | ;; => {:keys [ns inst data msg_ ...]} ; The signal 163 | ``` 164 | 165 | - [`with-signal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signal) will return the **last** signal created by the given form. 166 | - [`with-signals`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signals) will return **all** signals created by the given form. 167 | 168 | Both have several options, see their docstrings (links above) for details. 169 | 170 | ## Filtering 171 | 172 | A signal will be provided to a handler iff **ALL** of the following are true: 173 | 174 | - 1. Signal **call filters** pass: 175 | - a. Compile time: sample rate, kind, ns, id, level, when form, rate limit 176 | - b. Runtime: sample rate, kind, ns, id, level, when form, rate limit 177 | 178 | - 2. Signal **handler filters** pass: 179 | - a. Compile time: not applicable 180 | - b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit 181 | 182 | - 3. **Call transform** `(fn [signal]) => ?modified-signal` returns non-nil 183 | - 4. **Handler transform** `(fn [signal]) => ?modified-signal` returns non-nil 184 | 185 | > 👉 Transform fns provides a flexible way to modify and/or filter signals by arbitrary signal data/content conditions (return nil to skip handling). 186 | 187 | > 👉 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. 188 | 189 | Quick examples of some basic filtering: 190 | 191 | ```clojure 192 | (tel/set-min-level! :info) ; Set global minimum level 193 | (tel/with-signal (tel/log! {:level :info ...})) ; => {:keys [inst id ...]} 194 | (tel/with-signal (tel/log! {:level :debug ...})) ; => nil (signal not allowed) 195 | 196 | (tel/with-min-level :trace ; Override global minimum level 197 | (tel/with-signal (tel/log! {:level :debug ...})) ; => {:keys [inst id ...]} 198 | 199 | ;; Disallow all signals in matching namespaces 200 | (tel/set-ns-filter! {:disallow "some.nosy.namespace.*"}) 201 | ``` 202 | 203 | - Filtering is always O(1), except for rate limits which are O(n_windows). 204 | - See [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) for more about filtering. 205 | - See section [2-Architecture](./2-Architecture) for a flowchart / visual aid. 206 | 207 | # Internal help 208 | 209 | Telemere includes extensive internal help docstrings: 210 | 211 | | Var | Help with | 212 | | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | 213 | | [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | Creating signals | 214 | | [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options when creating signals | 215 | | [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal content (map given to transforms/handlers) | 216 | | [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation | 217 | | [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management | 218 | | [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options | 219 | | [`help:environmental-config`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:environmental-config) | Config via JVM properties, environment variables, or classpath resources | 220 | -------------------------------------------------------------------------------- /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 | Telemere signal flowchart 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.). -------------------------------------------------------------------------------- /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
)] 135 | (tel/log! {:perf-data perf-data} "Performance data")) 136 | ``` 137 | 138 | Telemere and Tufte work great together: 139 | 140 | - Their functionality is complementary. 141 | - The [upcoming](https://www.taoensso.com/roadmap) Tufte v3 will share the same core as Telemere and offer an **identical API** for managing filters and handlers. 142 | 143 | ## Truss 144 | 145 | > [Truss](https://www.taoensso.com/truss) is a micro toolkit for Clojure/Script errors by the author of Telemere. 146 | 147 | Telemere can easily incorporate Truss assertion failure information in its signals, just like any other (error) data. 148 | 149 | The [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) signal creator can be particularly convenient for this: 150 | 151 | ```clojure 152 | (tel/catch->error! ) 153 | ``` 154 | 155 | Telemere also uses [Truss contextual exceptions](https://cljdoc.org/d/com.taoensso/truss/CURRENT/api/taoensso.truss#ex-info) when relevant. -------------------------------------------------------------------------------- /wiki/4-Handlers.md: -------------------------------------------------------------------------------- 1 | Telemere's signal handlers are just **plain functions** that take a signal (map) to **do something with them** (analyse them, write them to console/file/queue/db/etc.). 2 | 3 | Here's a minimal handler: `(fn [signal] (println signal))`. 4 | 5 | A second 0-arg arity will be called when stopping the handler. This is handy for stateful handlers or handlers that need to release resources, e.g.: 6 | 7 | ``` 8 | (fn my-handler 9 | ([signal] (println signal) 10 | ([] (my-stop-code))) 11 | ``` 12 | 13 | Telemere includes a number of signal handlers out-the-box, and more may be available via the [community](./8-Community#handlers-and-tools). 14 | 15 | You can also easily [write your own handlers](#writing-handlers) for any output or integration you need. 16 | 17 | # Included handlers 18 | 19 | See ✅ links below for **features and usage**, 20 | See ❤️ links below to **vote on future handlers**: 21 | 22 | | Target (↓) | Clj | Cljs | 23 | | :--------------------------------------------- | :-----------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------: | 24 | | [Apache Kafka](https://kafka.apache.org/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 25 | | [AWS Kinesis](https://aws.amazon.com/kinesis/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 26 | | Console | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | 27 | | Console (raw) | - | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | 28 | | [Datadog](https://www.datadoghq.com/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | [❤️](https://github.com/taoensso/roadmap/issues/12) | 29 | | Email | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | - | 30 | | File/s | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | - | 31 | | [Graylog](https://graylog.org/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 32 | | [Jaeger](https://www.jaegertracing.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 33 | | [Logstash](https://www.elastic.co/logstash) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 34 | | [OpenTelemetry](https://opentelemetry.io/) | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry) | [❤️](https://github.com/taoensso/roadmap/issues/12) | 35 | | [Redis](https://redis.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 36 | | SQL | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 37 | | [Slack](https://slack.com/) | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) | - | 38 | | TCP socket | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | - | 39 | | UDP socket | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | - | 40 | | [Zipkin](https://zipkin.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - | 41 | 42 | # Configuring handlers 43 | 44 | There's two kinds of config relevant to all signal handlers: 45 | 46 | 1. **Dispatch** opts (common to all handlers), and 47 | 2. **Handler-specific** opts 48 | 49 | ## Dispatch opts 50 | 51 | Handler dispatch opts includes dispatch priority (determines order in which handlers are called), handler filtering, handler transform, a/sync queue semantics, back-pressure opts, etc. 52 | 53 | See [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) for full info, and [`default-handler-dispatch-opts`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#default-handler-dispatch-opts) for defaults. 54 | 55 | Note that the handler transform is an easily overlooked but powerful feature, allowing you to arbitrarily modify and/or filter every [signal map](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) before it is given to each handler. 56 | 57 | ## Handler-specific opts 58 | 59 | Handler-specific opts are specified when calling a particular **handler constructor** (like [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console)) - and documented by the constructor. 60 | 61 | Note that it's common for Telemere handlers to be customized by providing *Clojure/Script functions* to the relevant handler constructor call. 62 | 63 | See the [utils namespace](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils) for tools useful for customizing and writing signal handlers. 64 | 65 | ### Console handler 66 | 67 | The standard Clj/s console handler ([`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console)) writes signals **as strings** to `*out*`/`*err` or browser console. 68 | 69 | By default it writes formatted strings intended for human consumption: 70 | 71 | ```clojure 72 | ;; Create a test signal 73 | (def my-signal 74 | (tel/with-signal 75 | (tel/log! {:id ::my-id, :data {:x1 :x2}} "My message"))) 76 | 77 | ;; Create console handler with default opts (writes formatted string) 78 | (def my-handler (tel/handler:console {})) 79 | 80 | ;; Test handler, remember it's just a (fn [signal]) 81 | (my-handler my-signal) ; %> 82 | ;; 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My message 83 | ;; data: {:x1 :x2} 84 | ``` 85 | 86 | #### edn output 87 | 88 | To instead writes signals as [edn](https://github.com/edn-format/edn): 89 | 90 | ```clojure 91 | ;; Create console handler which writes signals as edn 92 | (def my-handler 93 | (tel/handler:console 94 | {:output-fn (tel/pr-signal-fn {:pr-fn :edn})})) 95 | 96 | (my-handler my-signal) ; %> 97 | ;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...} 98 | ``` 99 | 100 | #### JSON output 101 | 102 | To instead writes signals as JSON: 103 | 104 | ```clojure 105 | ;; Ref. (or any alt JSON lib) 106 | #?(:clj (require '[jsonista.core :as jsonista])) 107 | (def my-handler 108 | (tel/handler:console 109 | {:output-fn 110 | (tel/pr-signal-fn 111 | {:pr-fn 112 | #?(:cljs :json ; Use js/JSON.stringify 113 | :clj jsonista/write-value-as-string)})})) 114 | 115 | (my-handler my-signal) ; %> 116 | ;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...} 117 | ``` 118 | 119 | Note that when writing JSON with Clojure, you *must* provide an appropriate `pr-fn`. This lets you plug in the JSON serializer of your choice ([jsonista](https://github.com/metosin/jsonista) is my default recommendation). 120 | 121 | ### Handler-specific per-signal kvs 122 | 123 | Telemere includes a handy mechanism for including arbitrary app-level data/opts in individual signals for use by custom transforms and/or handlers. 124 | 125 | Any *non-standard* (app-level) keys you include in your signal constructor opts will automatically be included in created signals, e.g.: 126 | 127 | ```clojure 128 | (tel/with-signal 129 | (tel/log! 130 | {... 131 | :my-data-for-xfn "foo" 132 | :my-data-for-handler "bar"})) 133 | 134 | ;; %> 135 | ;; {;; App-level kvs included inline (assoc'd to signal root) 136 | ;; :my-data-for-xfn "foo" 137 | ;; :my-data-for-handler "bar" 138 | ;; :kvs ; And also collected together under ":kvs" key 139 | ;; {:my-data-for-xfn "foo" 140 | ;; :my-data-for-handler "bar"} 141 | ;; ... } 142 | ``` 143 | 144 | These app-level data/opts are typically NOT included by default in handler output, making them a great way to convey data/opts to custom transforms/handlers. 145 | 146 | # Managing handlers 147 | 148 | See [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) for info on signal handler management. 149 | 150 | ## Managing handlers on startup 151 | 152 | Want to add or remove a particular handler when your application starts? 153 | 154 | Just make an appropriate call to [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) or [`remove-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#remove-handler!). 155 | 156 | ### Environmental config 157 | 158 | If you want to manage handlers **conditionally** based on **environmental config** (JVM properties, environment variables, or classpath resources) - Telemere provides the highly flexible [`get-env`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-env) util. 159 | 160 | Use this to easily define your own arbitrary cross-platform config, and make whatever conditional handler management decisions you'd like. 161 | 162 | ## Stopping handlers 163 | 164 | Telemere supports complex handlers that may use internal state, buffers, etc. 165 | 166 | For this reason, you should **always manually call** [`stop-handlers!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#stop-handlers!) somewhere appropriate to give registered handlers the opportunity to flush buffers, close files, etc. 167 | 168 | The best place to do this is usually near the end of your application's `-main` or shutdown procedure, **AFTER** all other code has completed that could create signals. 169 | 170 | You can use [`call-on-shutdown!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#call-on-shutdown!) to create a JVM shutdown hook. 171 | 172 | Note that `stop-handlers!` will conveniently **block** to finish async handling of any pending signals. The max blocking time can be configured *per-handler* via the `:drain-msecs` [handler dispatch option](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) and defaults to 6 seconds. 173 | 174 | ## Handler stats 175 | 176 | By default, Telemere handlers maintain **comprehensive internal stats** including handling times and outcome counters. 177 | 178 | This can be **really useful** for debugging handlers, and understanding handler performance and back-pressure behaviour in practice. 179 | 180 | See [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) for an output example, etc. 181 | 182 | # Writing handlers 183 | 184 | Writing your own signal handlers for Telemere is straightforward, and a reasonable choice if you prefer customizing behaviour that way, or want to write signals to a DB/format/service for which a ready-made handler isn't available. 185 | 186 | - Signals are just plain Clojure/Script [maps](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content). 187 | - Handlers just plain Clojure/Script fns of 2 arities: 188 | 189 | ```clojure 190 | (defn my-handler 191 | ([signal] (println signal)) ; Arity-1 called when handling a signal 192 | ([] (my-stop-code)) ; Arity-0 called when stopping the handler 193 | ) 194 | ``` 195 | 196 | If you're making a customizable handler for use by others, it's often handy to define a handler **constructor**: 197 | 198 | ```clojure 199 | (defn handler:my-fancy-handler ; Note constructor naming convention 200 | "Needs `some-lib`, Ref. . 201 | 202 | Returns a signal handler that: 203 | - Takes a Telemere signal (map). 204 | - Does something useful with the signal! 205 | 206 | Options: 207 | `:option1` - Option description 208 | `:option2` - Option description 209 | 210 | Tips: 211 | - Tip 1 212 | - Tip 2" 213 | 214 | ([] (handler:my-fancy-handler nil)) ; Use default opts (iff defaults viable) 215 | ([{:as constructor-opts}] 216 | 217 | ;; Do option validation and other prep here, i.e. try to keep 218 | ;; expensive work outside handler function when possible! 219 | 220 | (let [handler-fn ; Fn of exactly 2 arities (1 and 0) 221 | (fn a-handler:my-fancy-handler ; Note fn naming convention 222 | 223 | ([signal] ; Arity-1 called when handling a signal 224 | ;; Do something useful with the given signal (write to 225 | ;; console/file/queue/db, etc.). Return value is ignored. 226 | ) 227 | 228 | ([] ; Arity-0 called when stopping the handler 229 | ;; Flush buffers, close files, etc. May just noop. 230 | ;; Return value is ignored. 231 | ))] 232 | 233 | ;; (Advanced, optional) You can use metadata to provide default 234 | ;; handler dispatch options (see `help:handler-dispatch-options`) 235 | 236 | (with-meta handler-fn 237 | {:dispatch-opts 238 | {:min-level :info 239 | :limit 240 | [[1 1000] ; Max 1 signal per second 241 | [10 60000] ; Max 10 signals per minute 242 | ]}})))) 243 | ``` 244 | 245 | - See [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) for signal map content. 246 | - See [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) for dispatch options. 247 | - See the [utils namespace](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils) for tools useful for customizing and writing signal handlers. 248 | - 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). 249 | - [PRs](https://github.com/taoensso/telemere/pulls) are **very welcome** for additions to Telemere's included handlers, or to Telemere's [community resources](./8-Community)! 250 | 251 | # Example output 252 | 253 | ```clojure 254 | (tel/log! {:id ::my-id, :data {:x1 :x2}} "My message") => 255 | ``` 256 | 257 | ## Clj console handler 258 | 259 | [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | string output: 260 | 261 | ``` 262 | 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My message 263 | data: {:x1 :x2} 264 | ``` 265 | 266 | ## Cljs console handler 267 | 268 | [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Chrome console: 269 | 270 | Default ClojureScript console handler output 271 | 272 | ## Cljs raw console handler 273 | 274 | [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Chrome console, with [cljs-devtools](https://github.com/binaryage/cljs-devtools): 275 | 276 | Raw ClojureScript console handler output 277 | 278 | ## Clj file handler 279 | 280 | [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | MacOS terminal: 281 | 282 | Default Clojure file handler output 283 | -------------------------------------------------------------------------------- /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). -------------------------------------------------------------------------------- /wiki/6-FAQ.md: -------------------------------------------------------------------------------- 1 | # Does Telemere replace Timbre? 2 | 3 | > [Timbre](https://www.taoensso.com/timbre) is a pure Clojure/Script logging library, and ancestor of Telemere. 4 | 5 | **Yes**, Telemere's functionality is a **superset of Timbre**, and offers *many* improvements over Timbre. 6 | 7 | But Timbre will **continue to be maintained and supported**, and will even receive some backwards-compatible improvements back-ported from Telemere. 8 | 9 | There is **no pressure to migrate** if you'd prefer not to. 10 | 11 | See section [5-Migrating](./5-Migrating#from-timbre) for migration info. 12 | 13 | # Why not just update Timbre? 14 | 15 | > [Timbre](https://www.taoensso.com/timbre) is a pure Clojure/Script logging library, and ancestor of Telemere. 16 | 17 | Why release Telemere as a *new library* instead of just updating Timbre? 18 | 19 | Timbre was first released 12+ years ago, and has mostly attempted to keep breaks in that time minimal. Which means that its fundamental design is now 12+ years old. 20 | 21 | I've learnt a lot since then, and would write Timbre differently if I were doing it again today. There's many refinements I've wanted to make over the years, but held back both because of the effort involved and because of not wanting to break Timbre users that are happy with it the way it is. 22 | 23 | Since receiving [open source funding](https://www.taoensso.com/my-work), undertaking larger projects became feasible - so I decided to experiment with a proof-of-concept rewrite free of all historical constraints. 24 | 25 | That eventually grew into Telemere. And I'm happy enough with the result that I feel confident in saying that there's nothing Timbre does better than Telemere, but plenty that Telemere does better than Timbre. Telemere is easier to use, faster, more robust, and significantly more flexible. It offers a better platform for what will be (I hope) the next many years of service. 26 | 27 | I will **continue to maintain and support** Timbre for users that are happy with it, though I've also tried to make [migration](./5-Migrating#from-timbre) as easy as possible. 28 | 29 | Over time, I also intend to back-port many backwards-compatible improvements from Telemere to Timbre. For one, Telemere's core was actually written as a library that can eventually be used by Telemere, Timbre, and also [Tufte](https://taoensso.com/tufte). 30 | 31 | This will eventually ease long-term maintenance, increase reliability, and help provide unified capabilities across all 3. 32 | 33 | # Does Telemere replace Tufte? 34 | 35 | > [Tufte](https://www.taoensso.com/tufte) is a simple performance monitoring library for Clojure/Script by the author of Telemere. 36 | 37 | **No**, Telemere does **not** replace [Tufte](https://www.taoensso.com/tufte). They work great together, and the [upcoming](https://www.taoensso.com/roadmap) Tufte v3 will share the same core as Telemere and offer an **identical API** for managing filters and handlers. 38 | 39 | There is **some feature overlap** though since Telemere offers basic performance measurement as part of its tracing features. 40 | 41 | For comparison: 42 | 43 | - Telemere offers dynamic profiling of a single form to a simple `:runtime-nsecs`. 44 | - Tufte offers dynamic and thread-local profiling of *arbitrary nested forms* to *detailed and mergeable runtime stats*. 45 | 46 | Basically, Tufte has much richer performance monitoring capabilities. 47 | 48 | They're focused on complementary things. When both are in use: 49 | 50 | - Tufte can be used for detailed performance measurement, and 51 | - Telemere can be used for conveying (aggregate) performance information as part of your system's general observability signals. 52 | 53 | # Does Telemere work with GraalVM? 54 | 55 | > [GraalVM](https://en.wikipedia.org/wiki/GraalVM) is a JDK alternative with ahead-of-time compilation for faster app initialization and improved runtime performance, etc. 56 | 57 | **Yes**, this shouldn't be a problem. 58 | 59 | # Does Telemere work with Babashka? 60 | 61 | > [Babashka](https://github.com/babashka/babashka) is a native Clojure interpreter for scripting with fast startup. 62 | 63 | **No**, not currently - though support should be possible with a little work. The current bottleneck is a dependency on [Encore](https://github.com/taoensso/encore), which uses some classes not available in Babashka. With some work it should be possible to remove the dependency, and so also reduce library size. 64 | 65 | If there's interest in this, please [upvote](https://github.com/taoensso/roadmap/issues/22) on my open source roadmap. 66 | 67 | # Why no format-style messages? 68 | 69 | Telemere's message API can do everything that traditional print *or* format style message builders can do but **much more flexibly** - and with pure Clojure/Script (so no arcane pattern syntax). 70 | 71 | To coerce/format/prepare args, just use the relevant Clojure/Script utils. 72 | 73 | **Signal messages are always lazy** (as are a signal's `:let` and `:data` [options](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options)), so you only pay the cost of arg prep and message building *if/when a signal is actually created* (i.e. after filtering, sampling, rate limiting, etc.). 74 | 75 | Examples: 76 | 77 | ```clojure 78 | ;; A fixed message (string arg) 79 | (tel/log! "A fixed message") ; %> {:msg "A fixed message"} 80 | 81 | ;; A joined message (vector arg) 82 | (let [user-arg "Bob"] 83 | (tel/log! ["User" (str "`" user-arg "`") "just logged in!"])) 84 | ;; %> {:msg_ "User `Bob` just logged in!` ...} 85 | 86 | ;; With arg prep 87 | (let [user-arg "Bob" 88 | usd-balance-str "22.4821"] 89 | 90 | (tel/log! 91 | {:let 92 | [username (clojure.string/upper-case user-arg) 93 | usd-balance (parse-double usd-balance-str)] 94 | 95 | :data 96 | {:username username 97 | :usd-balance usd-balance}} 98 | 99 | ["User" username "has balance:" (str "$" (Math/round usd-balance))])) 100 | 101 | ;; %> {:msg "User BOB has balance: $22" ...} 102 | 103 | (tel/log! (str "This message " "was built " "by `str`")) 104 | ;; %> {:msg "This message was built by `str`"} 105 | 106 | (tel/log! (format "This message was built by `%s`" "format")) 107 | ;; %> {:msg "This message was built by `format`"} 108 | ``` 109 | 110 | Note that you can even use `format` or any other formatter/s of your choice. Your signal message is the result of executing code, so build it however you want. 111 | 112 | See also [`msg-skip`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#msg-skip) and [`msg-splice`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#msg-splice) for some handy utils. 113 | 114 | # How to use Telemere from a library? 115 | 116 | See section [9-Authors](./9-Authors.md). 117 | 118 | # How does Telemere compare to μ/log? 119 | 120 | > [μ/log](https://github.com/BrunoBonacci/mulog) is an excellent "micro-logging library" for Clojure that shares many of the same capabilities and objectives as Telemere. 121 | 122 | Some **similarities** between Telemere and μ/log: 123 | 124 | - Both emphasize **structured data** rather than string messages 125 | - Both offer **tracing** to understand (nested) program flow 126 | - Both offer a (nested) **context** mechanism for arb application state 127 | - Both are **fast** and offer **async handling** 128 | - Both offer a variety of **handlers** and are designed for ease of use 129 | 130 | Some particular **strengths of μ/log** that I'm aware of: 131 | 132 | - More **established/mature** 133 | - Wider **range of handlers** (incl. Kafka, Kinesis, Prometheus, Zipkin, etc.) 134 | - More **community resources** (videos, guides, users, etc.) 135 | - **Smaller code** base (Telemere currently depends on [Encore](https://github.com/taoensso/encore)) 136 | - There may be others! 137 | 138 | Some particular **strengths of Telemere**: 139 | 140 | - Both **Clj and Cljs support** (μ/log is Clj only) 141 | - Rich **filtering capabilities** (see [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters)) incl. compile-time elision 142 | - Rich **dispatch control** (see [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options)) 143 | - Rich **environmental config** (see [`help:environmental-config`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:environmental-config)) for all platforms 144 | - Detailed **handler stats** (see [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats)) 145 | - Single **unified API** for all telemetry and traditional logging needs (see [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators)) 146 | - Lazy `:let`, `:data`, `:msg`, `:do` - evaluated only **after filtering** 147 | - Extensive [in-IDE documentation](./1-Getting-started#internal-help) 148 | 149 | **My subjective thoughts**: 150 | 151 | μ/log is an awesome, well-designed library with quality documentation and a solid API. It's **absolutely worth checking out** - you may well prefer it to Telemere! 152 | 153 | The two libraries have many shared capabilities and objectives. 154 | 155 | Ultimately I wrote Telemere because: 156 | 157 | 1. I have some particular needs, including very complex and large-scale applications that benefit from the kind of flexibility that Telemere offers re: filtering, dispatch, environmental config, lazy (post-filter) evaluation, etc. 158 | 2. I have some particular tastes re: my ideal API. 159 | 3. I wanted something that integrated particularly well with [Tufte](https://taoensso.com/tufte) and could share an identical API for filtering, handlers, etc. 160 | 4. I wanted a modern replacement for [Timbre](https://www.taoensso.com/timbre) users that offered a superset of its functionality and an [easy migration path](./5-Migrating#from-timbre). 161 | 162 | # Why the unusual arg order for `event!`? 163 | 164 | For their 2 arg arities, every standard signal creator _except_ [event!](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) takes an opts map as its _first_ argument. 165 | 166 | Why the apparent inconsistency? 167 | 168 | It's an intentional trade-off. `event!` is unique in 3x ways: 169 | 170 | 1. Its primary argument is typically very short (just an id keyword). 171 | 2. Its primary argument never depends on `:let` bindings. 172 | 3. Its opts typically include long or even multi-lined `:data`. 173 | 174 | If `event!` shared the same arg order as other signal creators, the common case would be something like `(event! {:data } ::dangling-id)` which gets unnecessarily awkward and doesn’t read well IMO. I want to know what event we’re talking about, before you tell me about the associated data. 175 | 176 | In contrast, creators like `log!` both tend to have a large/r primary argument (message) - and their primary argument often depends on `:let` bindings - e.g. `(log! {:id ::my-id, :let […]} )`. In these cases it reads much clearer to go left->right. We start with an id, specify some data, then use that data to construct a message. 177 | 178 | So basically the choice in trade-off was: 179 | 180 | 1. Prefer **consistency**, or 181 | 2. Prefer **ergonomics** of the common case usage 182 | 183 | I went with option 2 for several reasons: 184 | 185 | - There _is_ actually consistency, it’s just not as obvious - the typically-larger argument always goes _last_. 186 | - Most IDEs generally do a good job of reminding about the arg order. 187 | - The same trade-off may come up again in future for other new signal kinds, and I prefer that we adopt the pattern of optimising for common-case ergonomics. 188 | - One can always easily call `signal!` directly - this takes a single map arg, so lets you easily specify all args in preferred order. (I tend to exclusively use `signal!` myself since I prefer this flexibility). 189 | 190 | If there’s popular demand, I’d also be happy to add something like `ev!` which could choose the alternative trade-off. Though I’d recommend folks try `event!` as-is first, since I think the initial aversion/surprise might wear off with use. 191 | 192 | # Other questions? 193 | 194 | Please [open a Github issue](https://github.com/taoensso/telemere/issues) or ping on Telemere's [Slack channel](https://www.taoensso.com/telemere/slack). I'll regularly update the FAQ to add common questions. - [Peter](https://www.taoensso.com) 195 | -------------------------------------------------------------------------------- /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 | Telemere sampling 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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------