Compare commits

...

11 commits

Author SHA1 Message Date
Peter Taoussanis
4011560c60 v1.0.0-beta6 (2024-05-05) 2024-05-05 13:17:58 +02:00
Peter Taoussanis
d62bab2247 [mod] Rename, refactor signal formatting utils
- Simplified some util name (only relevant to folks customizing handler behaviour)
- Merged `format-signal->edn-fn`, `format-signal->json-fn` to single `pr-signal-fn`
2024-05-05 13:17:58 +02:00
Peter Taoussanis
7b7782e340 [doc] Doc improvements 2024-05-05 12:28:44 +02:00
Peter Taoussanis
ea6a039980 [new] Add :incl-kvs? opt to edn and JSON formatters 2024-05-05 12:28:44 +02:00
Peter Taoussanis
3e1f453d06 [new] Add :incl-thread?, :incl-kvs? opts to format-signal->str-fn 2024-05-05 12:28:44 +02:00
Peter Taoussanis
eed702a480 [new] Add :end-with-newline opt to signal formatters
Instead allow format-signal-fn to decide whether or not to end with a newline
2024-05-05 12:28:44 +02:00
Peter Taoussanis
cf22ddf861 [new] Add TCP, UDP socket handlers 2024-05-05 12:28:44 +02:00
Peter Taoussanis
cbd786be66 [fix] Broken postal handler subject 2024-05-03 14:09:53 +02:00
Peter Taoussanis
b6e8c5fd4a [new] Add experimental :thread key to Clj signals
Only downside/hesitation is that this info *must* be collected at the callsite,
which means that it affects the performance of *all* created signals.

Adds ~30-50 nsecs per signal.
2024-05-03 14:09:53 +02:00
Peter Taoussanis
b271c4f7b8 [mod] Simplify middleware - don't auto compose
Previously:

  It was possible to provide a vector of middleware fns when creating
  signals or adding handlers, e.g.:

    (event! ::ev-id1 {:middleware [fn1 fn2 ...]}),
    (add-handler! ::handler-id1 <handler-fn> {:middleware [fn1 fn2 ...]})

After this commit:

  Middleware is always expected to be a single fn, or nil.
  A `comp-middleware` util has been added to make it easy to compose multiple
  middleware fns into one.

Motivation:

  The previous (auto-composition) behaviour was nice when adding handlers,
  but incurred a (small but non-trivial) runtime cost when creating signals.

  The actual benefit (convenience) of auto-composition is very small, so
  I've decided to just remove this feature and add the `comp-middleware` util.

  Note that I ruled out the option of doing auto-comp only when adding handlers
  since that've meant an inconsistency and so complexity for little benefit.
2024-05-03 14:09:53 +02:00
Peter Taoussanis
195c16b476 [nop] Housekeeping 2024-05-03 14:09:53 +02:00
21 changed files with 680 additions and 301 deletions

View file

@ -2,6 +2,50 @@ This project uses [**Break Versioning**](https://www.taoensso.com/break-versioni
---
# `v1.0.0-beta6` (2024-05-05)
> **Dep/s**: [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-beta6) and [Telemere SLF4J provider](https://clojars.org/com.taoensso/slf4j-telemere/versions/1.0.0-beta6) are on Clojars.
> **Versioning**: Telemere uses [Break Versioning](https://www.taoensso.com/break-versioning).
This is a **maintenance pre-release** intended to fix issues that have come up during the beta. See below for details, and please **report any unexpected problems** on [GitHub](https://github.com/taoensso/telemere/issues) or the [Slack channel](https://www.taoensso.com/telemere/slack), thank you! 🙏
\- Peter Taoussanis
## Changes since `v1.0.0-beta1` (2024-04-19)
* d0a15bac [mod] Don't auto add OpenTelemetry handler
* 6d545dfc [mod] Move (simplify) OpenTelemetry ns
* c4d9dd09 [mod] Don't include user-level kvs in default signal content handler
* d3c63e17 [mod] Rename `clojure.tools.logging` sys val
* b271c4f7 [mod] Simplify middleware - don't auto compose
## Fixes since `v1.0.0-beta1` (2024-04-19)
* ffea1a30 [fix] Fix broken AOT support, add AOT tests
* e222297a [fix] SLF4J broken timestamps, add tests
## New since `v1.0.0-beta1` (2024-04-19)
* 2ba23ee7 [new] Add postal (email) handler
* cf22ddf8 [new] Add TCP, UDP socket handlers
* b6e8c5fd [new] Add experimental `:thread` key to Clj signals
* Handlers will now drain their signal queues on shutdown (configurable)
* Rate limiter performance improvements (via Encore)
* Doc improvements based on questions that've come up on Slack, etc.
## Everything since `v1.0.-beta5` (2024-04-29)
* ae823f0d [mod] Rename, refactor signal formatting utils
* b271c4f7 [mod] Simplify middleware - don't auto compose
* cbd786be [fix] Broken postal handler subject
* cf22ddf8 [new] Add TCP, UDP socket handlers
* b6e8c5fd [new] Add experimental `:thread` key to Clj signals
* ea6a0399 [new] Add `:incl-kvs?` opt to edn and JSON formatters
* 3e1f453d [new] Add `:incl-thread?`, `:incl-kvs?` opts to `format-signal->str-fn`
* eed702a4 [new] Add `:end-with-newline` opt to signal formatters
---
# `v1.0.0-beta5` (2024-04-29)
> **Dep/s**: [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-beta5) and [Telemere SLF4J provider](https://clojars.org/com.taoensso/slf4j-telemere/versions/1.0.0-beta5) are on Clojars.

View file

@ -32,6 +32,7 @@ See [here][GitHub releases] for earlier releases.
- 1st-class **out-the-box interop** with [SLF4J v2](https://www.slf4j.org/), [clojure.tools.logging](https://github.com/clojure/tools.logging), [OpenTelemetry](https://opentelemetry.io/), and [Tufte](https://www.taoensso.com/tufte).
- Included [shim](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.timbre) for easy/gradual [migration from Timbre](../../wiki/5-Migrating).
- Included [handlers](../../wiki/4-Handlers#included-handlers) for consoles, files, email, Redis, Slack, TCP/UDP sockets, Logstash, etc.
#### Scaling
@ -132,15 +133,14 @@ See relevant docstrings (links below) for usage info-
### Internal help
| Var | Help with |
| :-------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | List of signal creators |
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options for signal creators |
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal map content |
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
| Var | Help with |
| :---------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | List of signal creators |
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options for signal creators |
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal map content |
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
### Example handler output

View file

@ -141,17 +141,19 @@
;; Create console which writes signals as edn
(def my-handler
(t/handler:console
{:format-signal-fn (taoensso.telemere.utils/format-signal->edn-fn)}))
{:output-fn (t/pr-signal-fn :edn)}))
(my-handler my-signal) ; =>
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}
;; Create console which writes signals as JSON
#?(:clj (require '[jsonista.core :as jsonista]))
(def my-handler
(t/handler:console
{:format-signal-fn
(taoensso.telemere.utils/format-signal->json-fn
{:pr-json-fn jsonista.core/write-value-as-string})}))
{:output-fn
(t/pr-signal-fn
#?(:cljs :json
:clj jsonista.core/write-value-as-string))}))
(my-handler my-signal) ; =>
;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...}
@ -219,3 +221,19 @@
(t/log! (format "This message was built by `%s`" "format"))
;; %> {:msg "This message was built by `format`"}
;;; User kvs
(t/with-signal
(t/event! ::my-id
{:my-middleware-data "foo"
:my-handler-data "bar"}))
;; %>
;; {;; User kvs included inline (assoc'd to signal root)
;; :my-middleware-data "foo"
;; :my-handler-data "bar"
;; :kvs ; And also collected together under ":kvs" key
;; {:my-middleware-data "foo"
;; :my-handler-data "bar"}
;; ... }

View file

@ -1,4 +1,4 @@
(defproject com.taoensso/telemere "1.0.0-beta5"
(defproject com.taoensso/telemere "1.0.0-beta6"
:author "Peter Taoussanis <https://www.taoensso.com>"
:description "Structured telemetry library for Clojure/Script"
:url "https://www.taoensso.com/telemere"
@ -8,7 +8,7 @@
:url "https://www.eclipse.org/legal/epl-v10.html"}
:dependencies
[[com.taoensso/encore "3.105.1"]]
[[com.taoensso/encore "3.106.0"]]
:test-paths ["test" #_"src"]
@ -16,7 +16,7 @@
{;; :default [:base :system :user :provided :dev]
:provided {:dependencies [[org.clojure/clojurescript "1.11.132"]
[org.clojure/clojure "1.11.3"]]}
:c1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha10"]]}
:c1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"]]}
:c1.11 {:dependencies [[org.clojure/clojure "1.11.3"]]}
:c1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}
@ -45,13 +45,16 @@
[[org.clojure/test.check "1.1.1"]
[org.clojure/tools.logging "1.3.0"]
[org.slf4j/slf4j-api "2.0.13"]
[com.taoensso/slf4j-telemere "1.0.0-beta5"]
[com.taoensso/slf4j-telemere "1.0.0-beta6"]
#_[org.slf4j/slf4j-simple "2.0.13"]
#_[org.slf4j/slf4j-nop "2.0.13"]
[com.draines/postal "2.0.5"]
;;; For optional handlers
[io.opentelemetry/opentelemetry-api "1.37.0"]
#_[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.37.0"]
#_[io.opentelemetry/opentelemetry-exporter-otlp "1.37.0"]]
#_[io.opentelemetry/opentelemetry-exporter-otlp "1.37.0"]
[metosin/jsonista "0.3.8"]
[com.draines/postal "2.0.5"]]
:plugins
[[lein-pprint "1.3.2"]

View file

@ -26,6 +26,7 @@ Default signal keys:
`:line` -------- ?int line of signal creator callsite, same as (:line location)
`:column` ------ ?int column of signal creator callsite, same as (:column location)
`:file` -------- ?str filename of signal creator callsite, same as (:file location)
`:thread` ------ (Clj only) {:keys [group name id]} thread info for thread that called signal creator
`:sample-rate` - ?rate ∈ℝ[0,1] for combined signal AND handler sampling (0.75 => allow 75% of signals, nil => allow all)
<kvs> ---------- Other arb user-level ?kvs given to signal creator. Typically NOT included

View file

@ -1,6 +0,0 @@
Common signal formatters include:
(utils/format-signal-str->fn) {<opts>}) ; For human-readable string output (default)
(utils/format-signal->edn-fn) {<opts>}) ; For edn output
(utils/format-signal->json-fn {<opts>}) ; For JSON output
See relevant docstrings for details.

View file

@ -22,7 +22,7 @@ Signal options (shared by all signal creators):
`:sample-rate` - ?rate ∈ℝ[0,1] for signal sampling (0.75 => allow 75% of signals, nil => allow all)
`:when` -------- Arb ?form; when present, form must return truthy to allow signal
`:rate-limit` -- ?spec as given to `taoensso.telemere/rate-limiter`, see its docstring for details
`:middleware` -- ?[(fn [signal])=>modified-signal ...] signal middleware
`:middleware` -- Optional (fn [signal]) => ?modified-signal to apply when signal is created
`:trace?` ------ Should tracing be enabled for `:run` form?
<kvs> ---------- Other arb user-level ?kvs to incl. in signal. Typically NOT included in

View file

@ -1,7 +1,7 @@
{;;:lein true
:source-paths ["src" "test"]
:dependencies
[[com.taoensso/encore "3.105.1"]
[[com.taoensso/encore "3.106.0"]
[cider/cider-nrepl "0.47.0"]
[binaryage/devtools "1.0.7"]]

View file

@ -1,4 +1,4 @@
(defproject com.taoensso/slf4j-telemere "1.0.0-beta5"
(defproject com.taoensso/slf4j-telemere "1.0.0-beta6"
:author "Peter Taoussanis <https://www.taoensso.com>"
:description "Telemere backend/provider for SLF4J API v2"
:url "https://www.taoensso.com/telemere"
@ -16,7 +16,7 @@
{:dependencies
[[org.clojure/clojure "1.11.3"]
[org.slf4j/slf4j-api "2.0.13"]
[com.taoensso/telemere "1.0.0-beta5"]]}
[com.taoensso/telemere "1.0.0-beta6"]]}
:dev
{:plugins

View file

@ -32,9 +32,10 @@
(remove-ns 'taoensso.telemere)
(:api (enc/interns-overview)))
(enc/assert-min-encore-version [3 105 1])
(enc/assert-min-encore-version [3 106 0])
;;;; TODO
;; - Add handlers: Logstash, Slack, Carmine, Datadog, Kafka
;; - Native OpenTelemetry traces and spans
;; - Update Tufte (signal API, config API, signal keys, etc.)
;; - Update Timbre (signal API, config API, signal keys, backport improvements)
@ -71,13 +72,16 @@
enc/chance
enc/rate-limiter
enc/newline
enc/comp-middleware
impl/msg-splice
impl/msg-skip
#?(:clj impl/with-signal)
#?(:clj impl/with-signals)
#?(:clj impl/signal!)
utils/error-signal?)
utils/error-signal?
utils/pr-signal-fn
utils/format-signal-fn)
;;;; Help
@ -87,7 +91,6 @@
(impl/defhelp help:signal-content :signal-content)
(enc/defalias help:signal-filters help:filters) ; Via Encore
(enc/defalias help:signal-handlers help:handlers) ; Via Encore
(impl/defhelp help:signal-formatters :signal-formatters)
;;;; Context
@ -118,12 +121,12 @@
#?(:clj
(defmacro set-ctx!
"Set `*ctx*` var's root (base) value. See `*ctx*` for details."
[root-val] `(enc/set-var-root! *ctx* ~root-val)))
[root-ctx-val] `(enc/set-var-root! *ctx* ~root-ctx-val)))
#?(:clj
(defmacro with-ctx
"Evaluates given form with given `*ctx*` value. See `*ctx*` for details."
[init-val form] `(binding [*ctx* ~init-val] ~form)))
[ctx-val form] `(binding [*ctx* ~ctx-val] ~form)))
(comment (with-ctx "my-ctx" *ctx*))
@ -142,14 +145,13 @@
(comment (with-ctx {:a :A1 :b :B1} (with-ctx+ {:a :A2} *ctx*)))
;;;; Middleware
;;;; Signal middleware
(enc/defonce ^:dynamic *middleware*
"Optional vector of unary middleware fns to apply (sequentially/left-to-right)
to each signal before passing it to handlers. If any middleware fn returns nil,
aborts immediately without calling handlers.
"Optional (fn [signal]) => ?modified-signal to apply (once) when
signal is created. When middleware returns nil, skips all handlers.
Useful for transforming each signal before handling.
Compose multiple middleware fns together with `comp-middleware.
Re/bind dynamic value using `with-middleware`, `binding`.
Modify root (base) value using `set-middleware!`."
@ -158,13 +160,13 @@
#?(:clj
(defmacro set-middleware!
"Set `*middleware*` var's root (base) value. See `*middleware*` for details."
[root-val] `(enc/set-var-root! *middleware* ~root-val)))
[?root-middleware-fn] `(enc/set-var-root! *middleware* ~?root-middleware-fn)))
#?(:clj
(defmacro with-middleware
"Evaluates given form with given `*middleware*` value.
See `*middleware*` for details."
[init-val form] `(binding [*middleware* ~init-val] ~form)))
[?middleware-fn form] `(binding [*middleware* ~?middleware-fn] ~form)))
;;;; Signal creators
;; - signal! [ opts] ; => allowed? / run result (value or throw)

View file

@ -18,20 +18,19 @@
Returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Writes a formatted signal string to stream.
- Writes formatted signal string to stream.
A general-purpose `println`-style handler that's well suited for outputting
signals formatted as edn, JSON, or human-readable strings.
Options:
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`
`:stream` - `java.io.writer`
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
`:stream` - `java.io.writer`
Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise."
([] (handler:console nil))
([{:keys [format-signal-fn stream]
:or {format-signal-fn (utils/format-signal->str-fn)}}]
([{:keys [output-fn stream]
:or {output-fn (utils/format-signal-fn)}}]
(let [stream (case stream :*out* *out*, :*err* *err* stream)
error-signal? utils/error-signal?
@ -42,8 +41,8 @@
([signal]
(let [^java.io.Writer stream
(or stream (if (error-signal? signal) *err* *out*))]
(when-let [output (format-signal-fn signal)]
(.write stream (str output nl))
(when-let [output (output-fn signal)]
(.write stream (str output))
(.flush stream))))))))
:cljs
@ -52,17 +51,17 @@
If `js/console` exists, returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Writes a formatted signal string to JavaScript console.
- Writes formatted signal string to JavaScript console.
A general-purpose `println`-style handler that's well suited for outputting
signals formatted as edn, JSON, or human-readable strings.
Options:
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`"
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`"
([] (handler:console nil))
([{:keys [format-signal-fn]
:or {format-signal-fn (utils/format-signal->str-fn)}}]
([{:keys [output-fn]
:or {output-fn (utils/format-signal-fn)}}]
(when (exists? js/console)
(let [js-console-logger utils/js-console-logger
@ -71,9 +70,9 @@
(fn a-handler:console
([]) ; Shut down (no-op)
([signal]
(when-let [output (format-signal-fn signal)]
(when-let [output (output-fn signal)]
(let [logger (js-console-logger (get signal :level))]
(.call logger logger (str output nl)))))))))))
(.call logger logger (str output)))))))))))
#?(:cljs
(defn- logger-fn [logger]
@ -96,16 +95,16 @@
Ref. <https://github.com/binaryage/cljs-devtools>."
([] (handler:console-raw nil))
([{:keys [format-signal->prelude-fn format-nsecs-fn] :as opts
([{:keys [preamble-fn format-nsecs-fn] :as opts
:or
{format-signal->prelude-fn (utils/format-signal->prelude-fn) ; (fn [signal])
format-nsecs-fn (utils/format-nsecs-fn) ; (fn [nanosecs])
{preamble-fn (utils/signal-preamble-fn)
format-nsecs-fn (utils/format-nsecs-fn) ; (fn [nanosecs])
}}]
(when (and (exists? js/console) (exists? js/console.group))
(let [js-console-logger utils/js-console-logger
signal-content-handler ; (fn [signal hf vf]
(utils/signal-content-handler
content-fn ; (fn [signal append-fn val-fn])
(utils/signal-content-fn
{:format-nsecs-fn format-nsecs-fn
:format-error-fn nil
:raw-error? true})]
@ -117,8 +116,8 @@
logger (js-console-logger level)]
;; Unfortunately groups have no level
(.group js/console (format-signal->prelude-fn signal))
(signal-content-handler signal (logger-fn logger) identity)
(.group js/console (preamble-fn signal))
(content-fn signal (logger-fn logger) identity)
(when-let [stack (and error (.-stack (enc/ex-root error)))]
(.call logger logger stack))

View file

@ -270,7 +270,7 @@
Returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Writes a formatted signal string to file.
- Writes formatted signal string to file.
Signals will be appended to file specified by `path`.
Depending on options, archives may be maintained:
@ -284,9 +284,10 @@
`/logs/telemere.log-2020-01-01m.8.gz` ; Archive for Jan 2020, part 8 (oldest entries)
Options:
`:format-signal-fn`- (fn [signal]) => output, see `help:signal-formatters`.
`:path` - Path string of the target output file (default `logs/telemere.log`).
`:interval` - #{nil :daily :weekly :monthly} (default `:monthly`).
`:output-fn`- (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
`:path` - Path string of the target output file (default `logs/telemere.log`)
`:interval` - #{nil :daily :weekly :monthly} (default `:monthly`)
When non-nil, causes interval-based archives to be maintained.
`:max-file-size` #{nil <pos-int>} (default 4MB)
@ -300,7 +301,7 @@
([] (handler:file nil))
([{:keys
[format-signal-fn
[output-fn
path interval
max-file-size
max-num-parts
@ -308,7 +309,7 @@
gzip-archives?]
:or
{format-signal-fn (utils/format-signal->str-fn)
{output-fn (utils/format-signal-fn)
path "logs/telemere.log" ; Main path, we'll ALWAYS write to this exact file
interval :monthly
max-file-size (* 1024 1024 4) ; 4MB
@ -362,9 +363,8 @@
(fn a-handler:file
([] (locking lock (fw))) ; Close writer
([signal]
(when-let [output (format-signal-fn signal)]
(let [output-str (str output utils/newline)
new-interval? (when interval (new-interval!?))
(when-let [output (output-fn signal)]
(let [new-interval? (when interval (new-interval!?))
>max-file-size? (when max-file-size (>max-file-size?))
reset-stream? (or new-interval? >max-file-size?)]
@ -387,7 +387,7 @@
max-num-parts gzip-archives? nil)))
(when reset-stream? (fw :writer/reset!))
(do (fw output-str))))))))))
(do (fw output))))))))))
(comment
(manage-test-files! :create)

View file

@ -201,14 +201,6 @@
(defmacro with-tracing
"Wraps `form` with tracing iff const boolean `trace?` is true."
[trace? id uid form]
;; Not much motivation to support runtime `trace?` form, but easy
;; to add support later if desired
(when-not (enc/const-form? trace?)
(enc/unexpected-arg! trace?
{:msg "Expected constant (compile-time) `:trace?` value"
:context `with-tracing}))
(if trace?
`(binding [*trace-parent* (TraceParent. ~id ~uid)] ~form)
(do form))))
@ -223,7 +215,7 @@
(defrecord Signal
;; Telemere's main public data type, we avoid nesting and duplication
[^long schema inst uid,
location ns line column file,
location ns line column file #?(:clj thread),
sample-rate, kind id level, ctx parent,
data msg_ error run-form run-val,
end-inst run-nsecs kvs]
@ -359,7 +351,7 @@
^Signal
;; Note all dynamic vals passed as explicit args for better control
[inst uid,
location ns line column file,
location ns line column file #?(:clj thread :cljs _thread),
sample-rate, kind id level, ctx parent,
kvs data msg_,
run-form run-result error]
@ -379,14 +371,14 @@
msg_)]
(Signal. 1 inst uid,
location ns line column file,
location ns line column file #?(:clj thread),
sample-rate, kind id level, ctx parent,
data msg_,
run-err run-form run-val,
end-inst run-nsecs kvs))
(Signal. 1 inst uid,
location ns line column file,
location ns line column file #?(:clj thread),
sample-rate, kind id level, ctx parent,
data msg_, error nil nil nil nil kvs))]
@ -395,10 +387,10 @@
(do signal))))
(comment
(enc/qb 1e6 ; 55.67
(enc/qb 1e6 ; 66.8
(new-signal
nil nil nil nil nil nil nil nil nil nil
nil nil nil nil nil nil nil nil nil)))
nil nil nil nil nil nil nil nil nil nil)))
;;;; Signal API helpers
@ -548,6 +540,17 @@
;;;; Signal macro
#?(:clj
(defn thread-info
"Returns {:keys [group name id]} for current thread."
[]
(when-let [t (Thread/currentThread)]
{:group (when-let [g (.getThreadGroup t)] (.getName g))
:name (.getName t)
:id (.getId t)})))
(comment (enc/qb 1e6 (thread-info))) ; 44.49
#?(:clj
(defmacro ^:public signal!
"Generic low-level signal call, also aliased in Encore."
@ -557,6 +560,7 @@
(have? map? opts) ; We require const map keys, but vals may require eval
(let [defaults (get opts :defaults)
opts (merge defaults (dissoc opts :defaults))
clj? (not (:ns &env))
{run-form :run} opts
{:keys [#_expansion-id location elide? allow?]}
@ -580,15 +584,24 @@
kind-form :kind
id-form :id} opts
trace? (get opts :trace? (boolean run-form))
trace? (get opts :trace? (boolean run-form))
_
(when-not (contains? #{true false nil} trace?)
;; Not much motivation to support runtime `trace?` form, but easy
;; to add support later if desired
(enc/unexpected-arg! trace?
{:msg "Expected constant (compile-time) `:trace?` boolean"
:context `with-tracing}))
inst-form (get opts :inst :auto)
inst-form (if (= inst-form :auto) `(enc/now-inst*) inst-form)
inst-form (get opts :inst :auto)
inst-form (if (= inst-form :auto) `(enc/now-inst*) inst-form)
uid-form (get opts :uid (when trace? :auto/uuid))
uid-form (parse-uid-form uid-form)
uid-form (get opts :uid (when trace? :auto/uuid))
uid-form (parse-uid-form uid-form)
signal-form
thread-form (if clj? `(thread-info) nil)
signal-delay-form
(let [{do-form :do
let-form :let
msg-form :msg
@ -599,8 +612,9 @@
let-form (or let-form '[])
msg-form (parse-msg-form msg-form)
ctx-form (get opts :ctx `taoensso.telemere/*ctx*)
parent-form (get opts :parent (when trace? `taoensso.telemere.impl/*trace-parent*))
ctx-form (get opts :ctx `taoensso.telemere/*ctx*)
parent-form (get opts :parent (when trace? `taoensso.telemere.impl/*trace-parent*))
middleware-form (get opts :middleware `taoensso.telemere/*middleware*)
kvs-form
(not-empty
@ -610,6 +624,7 @@
:ctx :parent #_:trace?, :do :let :data :msg :error :run
:elide? :allow? #_:expansion-id))]
;; Compile-time validation
(when (and run-form error-form)
(throw ; Prevent ambiguity re: source of error
(ex-info "Signals cannot have both `:run` and `:error` opts at the same time"
@ -618,60 +633,57 @@
:location location
:other-opts (dissoc opts :run :error)})))
;; Eval let bindings AFTER call filtering but BEFORE data, msg
`(do
`(delay
;; Delay (cache) shared by all handlers. Covers signal `:let` eval, signal construction,
;; middleware (possibly expensive), etc. Throws here will be caught by handler.
~do-form
(let ~let-form ; Allow to throw during `signal-value_` deref
(new-signal ~'__inst ~'__uid
~location ~'__ns ~line-form ~column-form ~file-form,
~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form,
~kvs-form ~data-form ~msg-form,
'~run-form ~'__run-result ~error-form))))
(let [~@let-form ; Allow to throw, eval BEFORE data, msg, etc.
~'__signal
(new-signal ~'__inst ~'__uid
~location ~'__ns ~line-form ~column-form ~file-form ~'__thread,
~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form,
~kvs-form ~data-form ~msg-form,
'~run-form ~'__run-result ~error-form)]
run-fn-form (when run-form `(fn [] (~run-form)))]
;; Final unwrapped signal value visible to users/handler-fns, allow to throw
(if-let [sig-middleware# ~middleware-form]
(sig-middleware# ~'__signal) ; Apply signal middleware, can throw
(do ~'__signal)))))]
;; Could avoid double `run-form` expansion with a fn wrap (>0 cost)
;; `(let [~'run-fn-form ~run-fn-form]
;; (if-not ~allow?
;; (run-fn-form)
;; (let [...])))
;; (let [run-fn-form (when run-form `(fn [] (~run-form)))]
;; `(let [~'run-fn-form ~run-fn-form]
;; (if-not ~allow?
;; (run-fn-form)
;; (let [...]))))
`(enc/if-not ~allow? ; Allow to throw at call
~run-form
(let [~'__inst ~inst-form ; Allow to throw at call
~'__level ~level-form ; ''
~'__kind ~kind-form ; ''
~'__id ~id-form ; ''
~'__uid ~uid-form ; ''
~'__ns ~ns-form ; ''
(let [~'__inst ~inst-form ; Allow to throw at call
~'__level ~level-form ; ''
~'__kind ~kind-form ; ''
~'__id ~id-form ; ''
~'__uid ~uid-form ; ''
~'__ns ~ns-form ; ''
~'__thread ~thread-form ; ''
~'__call-middleware ~(get opts :middleware `taoensso.telemere/*middleware*)
~'__run-result ; Non-throwing (traps)
~(when run-form
`(let [~'__t0 (enc/now-nano*)]
`(let [t0# (enc/now-nano*)]
(with-tracing ~trace? ~'__id ~'__uid
(enc/try*
(do (RunResult. ~run-form nil (- (enc/now-nano*) ~'__t0)))
(catch :all ~'__t (RunResult. nil ~'__t (- (enc/now-nano*) ~'__t0)))))))
(do (RunResult. ~run-form nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#)))))))
~'__signal_
(delay
;; Cache shared by all handlers. Covers signal `:let` eval, signal construction,
;; middleware (possibly expensive), etc.
signal_# ~signal-delay-form]
;; The unwrapped signal value actually visible to users/handler-fns, realized only
;; AFTER handler filtering. Allowed to throw on deref (handler will catch).
(let [~'__signal ~signal-form] ; Can throw
(if ~'__call-middleware
((sigs/get-middleware-fn ~'__call-middleware) ~'__signal) ; Can throw
(do ~'__signal))))]
;; Unconditionally send same wrapped signal to all handlers.
;; Each handler will then use wrapper for filtering, unwrapping allowed signals.
(dispatch-signal! (WrappedSignal. ~'__ns ~'__kind ~'__id ~'__level ~'__signal_))
(dispatch-signal! ; Runner preserves dynamic bindings when async.
;; Unconditionally send same wrapped signal to all handlers. Each handler will
;; use wrapper for handler filtering, unwrapping (realizing) only allowed signals.
(WrappedSignal. ~'__ns ~'__kind ~'__id ~'__level signal_#))
(if ~'__run-result
(do (~'__run-result ~'__signal_))
(do (~'__run-result signal_#))
true))))))))
(comment

View file

@ -13,23 +13,23 @@
;;;; Implementation
(defn format-signal->subject-fn
(defn signal-subject-fn
"Experimental, subject to change.
Returns a (fn format [signal]) that:
- Takes a Telemere signal.
- Returns a formatted email subject like:
\"INFO EVENT :taoensso.telemere.postal/ev-id1 - msg\""
([] (format-signal->subject-fn nil))
([] (signal-subject-fn nil))
([{:keys [max-len subject-signal-key]
:or
{max-len 128
subject-signal-key :postal/subject}}]
(fn format-signal->subject [signal]
(fn signal-subject [signal]
(or
(get signal subject-signal-key) ; Custom subject
;; Simplified `format-signal->prelude-fn`
;; Simplified `utils/signal-preamble-fn`
(let [{:keys [level kind #_ns id msg_]} signal
sb (enc/str-builder)
s+spc (enc/sb-appender sb " ")]
@ -41,9 +41,7 @@
(enc/get-substr-by-len (str sb) 0 max-len))))))
(comment
((format-signal->subject-fn)
(tel/with-signal (tel/event! ::ev-id1 #_{:postal/subject "My subject"}))))
(comment ((signal-subject-fn) (tel/with-signal (tel/event! ::ev-id1 #_{:postal/subject "My subject"}))))
;;;; Handler
@ -55,7 +53,7 @@
Returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Sends an email with formatted signal content to the configured recipient.
- Sends formatted signal string as email to specified recipient.
Useful for emailing important alerts to admins, etc.
@ -80,8 +78,8 @@
:cc \"engineering@example.com\"
:X-MyHeader \"A custom header\"}
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`
`:format-signal->subject-fn` - (fn [signal]) => email subject string
`:subject-fn` - (fn [signal]) => email subject string
`:body-fn` - (fn [signal]) => email body content string, see `format-signal-fn` or `pr-signal-fn`
Tips:
@ -107,12 +105,12 @@
([{:keys
[postal/conn-opts
postal/msg-opts
format-signal-fn
format-signal->subject-fn]
subject-fn
body-fn]
:or
{format-signal-fn (utils/format-signal->str-fn)
format-signal->subject-fn (format-signal->subject-fn)}}]
{subject-fn (signal-subject-fn)
body-fn (utils/format-signal-fn)}}]
(when-not conn-opts (throw (ex-info "No `:postal/conn-opts` was provided" {})))
(when-not msg-opts (throw (ex-info "No `:postal/msg-opts` was provided" {})))
@ -121,12 +119,16 @@
(defn a-handler:postal
([]) ; Shut down (no-op)
([signal]
(let [msg
(assoc msg-opts
:subject (format-signal->subject-fn)
:body
[{:type "text/plain; charset=utf-8"
:content (format-signal-fn signal)}])
(enc/when-let [subject (subject-fn signal)
body (body-fn signal)]
(let [msg
(assoc msg-opts
:subject (str subject)
:body
(if (string? body)
[{:type "text/plain; charset=utf-8"
:content (str body)}]
body))
[result ex]
(try
@ -136,4 +138,4 @@
success? (= (get result :code) 0)]
(when-not success?
(throw (ex-info "Failed to send email" result ex)))))))))
(throw (ex-info "Failed to send email" result ex))))))))))

View file

@ -0,0 +1,106 @@
(ns taoensso.telemere.sockets
"Basic TCP/UDP socket handlers."
(:require
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.utils :as utils])
(:import
[java.net Socket InetAddress]
[java.net DatagramSocket DatagramPacket InetSocketAddress]
[java.io PrintWriter]))
(comment
(require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.sockets)
(:api (enc/interns-overview)))
;;;; Implementation
;;;; Handlers
(defn handler:tcp-socket
"Experimental, subject to change. Feedback welcome!
Returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Sends formatted signal string to specified TCP socket.
Options:
`host` - Destination TCP socket hostname string
`port` - Destination TCP socket port int
`:socket-opts` - {:keys [ssl? connect-timeout-msecs]}
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
Limitations:
- Failed writes will be retried only once.
- Writes lock on a single underlying socket, so IO won't benefit from adding
extra handler threads. Let me know if there's demand for socket pooling."
([host port] (handler:tcp-socket host port nil))
([host port
{:keys [socket-opts output-fn]
:or {output-fn (utils/format-signal-fn)}}]
(let [sw (utils/tcp-socket-writer host port socket-opts)]
(defn a-handler:tcp-socket
([] (sw)) ; Shut down
([signal]
(when-let [output (output-fn signal)]
(sw output)))))))
(defn handler:udp-socket
"Experimental, subject to change. Feedback welcome!
Returns a (fn handler [signal]) that:
- Takes a Telemere signal.
- Sends formatted signal string to specified UDP socket.
Options:
`host` - Destination UDP socket hostname string
`port` - Destination UDP socket port int
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
`:max-packet-bytes` - Max packet size (in bytes) before truncating output (default 512)
`:truncation-warning-fn` - Optional (fn [{:keys [max actual signal]}]) to call whenever
output is truncated. Should be appropriately rate-limited!
Limitations:
- Due to UDP limitations, truncates output to `max-packet-bytes`!
- Failed writes will be retried only once.
- Writes lock on a single underlying socket, so IO won't benefit from adding
extra handler threads. Let me know if there's demand for socket pooling.
- No DTLS (Datagram Transport Layer Security) support,
please let me know if there's demand."
([host port] (handler:udp-socket host port nil))
([host port
{:keys [output-fn max-packet-bytes truncation-warning-fn]
:or
{output-fn (utils/format-signal-fn)
max-packet-bytes 512}}]
(let [max-packet-bytes (int max-packet-bytes)
socket (DatagramSocket.) ; No need to change socket once created
lock (Object.)]
(.connect socket (InetSocketAddress. (str host) (int port)))
(defn a-handler:udp-socket
([] (.close socket)) ; Shut down
([signal]
(when-let [output (output-fn signal)]
(let [ba (enc/str->utf8-ba (str output))
ba-len (alength ba)
packet (DatagramPacket. ba (min ba-len max-packet-bytes))]
(when (and truncation-warning-fn (> ba-len max-packet-bytes))
;; Fn should be appropriately rate-limited
(truncation-warning-fn {:max max-packet-bytes, :actual ba-len, :signal signal}))
(locking lock
(try
(.send (DatagramSocket.) packet)
(catch Exception _ ; Retry once
(Thread/sleep 250)
(.send (DatagramSocket.) packet)))))))))))

View file

@ -4,7 +4,8 @@
(:require
[clojure.string :as str]
#?(:clj [clojure.java.io :as jio])
[taoensso.encore :as enc :refer [have have?]]))
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.impl :as impl]))
(comment
(require '[taoensso.telemere :as tel])
@ -52,7 +53,7 @@
;;;; Public misc
(enc/defaliases enc/newline enc/pr-edn #?(:cljs enc/pr-json))
(enc/defaliases enc/newline enc/pr-edn #?(:cljs enc/pr-json) #?(:clj impl/thread-info))
#?(:clj (defn thread-name "Returns string name of current thread." ^String [] (.getName (Thread/currentThread))))
#?(:clj (defn thread-id "Returns long id of current thread." ^long [] (.getId (Thread/currentThread))))
@ -249,7 +250,7 @@
:writer/state {:file file, :stream (.deref stream_)}
(when (open?_)
(let [content content-or-action
ba (.getBytes (str content) java.nio.charset.StandardCharsets/UTF_8)]
ba (enc/str->utf8-ba (str content))]
(locking lock
(try
(file-exists!)
@ -260,6 +261,133 @@
(comment (def fw1 (file-writer "test.txt" true)) (fw1 "x") (fw1))
;;;; Sockets
#?(:clj
(defn- default-socket-fn
"Returns conected `java.net.Socket`, or throws."
^java.net.Socket [host port connect-timeout-msecs]
(let [addr (java.net.InetSocketAddress. ^String host (int port))
socket (java.net.Socket.)]
(if connect-timeout-msecs
(.connect socket addr (int connect-timeout-msecs))
(.connect socket addr))
socket)))
#?(:clj
(let [factory_ (delay (javax.net.ssl.SSLSocketFactory/getDefault))]
(defn- default-ssl-socket-fn
"Returns connected SSL `java.net.Socket`, or throws."
^java.net.Socket [^java.net.Socket socket ^String host port]
(.createSocket ^javax.net.ssl.SSLSocketFactory @factory_
socket host (int port) true))))
#?(:clj
(defn tcp-socket-writer
"Experimental, subject to change. Feedback welcome!
Connects to specified TCP socket and returns a stateful fn of 2 arities:
[content] => Writes given content to socket, or no-ops if closed.
[] => Closes the writer.
Useful for basic handlers that write to a TCP socket, etc.
Options:
`:ssl?` - Use SSL/TLS?
`:connect-timeout-msecs` - Connection timeout (default 3000 msecs)
`:socket-fn` - (fn [host port timeout]) => `java.net.Socket`
`:ssl-socket-fn` - (fn [socket host port]) => `java.net.Socket`
Notes:
- Writer should be manually closed after use (with zero-arity call).
- Flushes after every write.
- Will retry failed writes once, then drop.
- Thread safe, locks on single socket stream.
- Advanced users may want a custom implementation using a connection
pool and/or more sophisticated retry semantics, etc."
[host port
{:keys
[ssl? connect-timeout-msecs,
socket-fn ssl-socket-fn] :as opts
:or
{connect-timeout-msecs 3000
socket-fn default-socket-fn
ssl-socket-fn default-ssl-socket-fn}}]
(let [new-conn! ; => [<java.net.Socket> <java.io.OutputStream>], or throws
(fn []
(try
(let [^java.net.Socket socket
(let [socket (socket-fn host port connect-timeout-msecs)]
(if ssl?
(ssl-socket-fn socket host port)
(do socket)))]
[socket (.getOutputStream socket)])
(catch Exception ex
(throw (ex-info "Failed to create connection" opts ex)))))
conn_ (volatile! (new-conn!))
open?_ (enc/latom true)
close!
(fn []
(when (compare-and-set! open?_ true false)
(when-let [[^java.net.Socket socket] (.deref conn_)]
(.close socket)
(vreset! conn_ nil)
true)))
reset!
(fn []
(close!)
(vreset! conn_ (new-conn!))
(reset! open?_ true)
true)
write-ba!
(fn [^bytes ba-content]
(when-let [[_ ^java.io.OutputStream output] (.deref conn_)]
(.write output ba-content)
(.flush output)
true))
conn-okay!
(let [rl (enc/rate-limiter-once-per 250)]
(fn []
(or
(rl)
(when-let [[^java.net.Socket socket] (.deref conn_)]
(and
(not (.isClosed socket))
(do (.isConnected socket))))
(throw (java.io.IOException. "Bad connection")))))
lock (Object.)]
(fn a-tcp-socket-writer
([] (when (open?_) (locking lock (close!))))
([content-or-action]
(case content-or-action ; Undocumented, for dev/testing
:writer/open? (open?_)
:writer/reset! (locking lock (reset!))
:writer/state {:conn (.deref conn_)}
(when (open?_)
(let [content content-or-action
ba (enc/str->utf8-ba (str content))]
(locking lock
(try
(conn-okay!)
(write-ba! ba)
(catch Exception _ ; Retry once
(reset!)
(write-ba! ba))))))))))))
;;;; Formatters
(defn format-nsecs-fn
@ -324,17 +452,21 @@
(do (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))
(println (str "--\n" ((format-error-fn) (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))))
(defn format-signal->prelude-fn
;;;;
(defn signal-preamble-fn
"Experimental, subject to change.
Returns a (fn format [signal]) that:
Returns a (fn preamble [signal]) that:
- Takes a Telemere signal.
- Returns a formatted prelude string like:
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere(2,21) ::ev-id - msg\""
([] (format-signal->prelude-fn nil))
- Returns a signal preamble string like:
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere(2,21) ::ev-id - msg\"
See arglists for options."
([] (signal-preamble-fn nil))
([{:keys [format-inst-fn]
:or {format-inst-fn (format-inst-fn)}}]
(fn format-signal->prelude [signal]
(fn signal-preamble [signal]
(let [{:keys [inst level kind ns id msg_]} signal
sb (enc/str-builder)
s+spc (enc/sb-appender sb " ")]
@ -358,129 +490,162 @@
(when-let [msg (force msg_)] (s+spc "- " msg))
(str sb)))))
(comment ((format-signal->prelude-fn) (tel/with-signal (tel/event! ::ev-id))))
(comment ((signal-preamble-fn) (tel/with-signal (tel/event! ::ev-id))))
(defn signal-content-fn
"Experimental, subject to change.
Returns a (fn content [signal]) that:
- Takes a Telemere signal.
- Returns a signal content string (incl. data, ctx, etc.)
See arglists for options."
([] (signal-content-fn nil))
([{:keys
[incl-thread? incl-kvs? raw-error?,
format-nsecs-fn format-error-fn]
(defn ^:no-doc signal-content-handler
"Private, don't use.
Returns a (fn handle [signal handle-fn value-fn]) for internal use.
Content equivalent to `format-signal->prelude-fn`."
([] (signal-content-handler nil))
([{:keys [format-nsecs-fn format-error-fn raw-error?]
:or
{format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
format-error-fn (format-error-fn) ; (fn [error])
}}]
(let [err-start (str newline "<<< error <<<" newline)
err-stop (str newline ">>> error >>>")]
(let [nl newline
err-start (str nl "<<< error <<<" nl)
err-stop (str nl ">>> error >>>")]
(fn a-signal-content-handler [signal hf vf]
(let [{:keys [uid parent data #_kvs ctx sample-rate]} signal]
(when sample-rate (hf "sample: " (vf sample-rate)))
(when uid (hf " uid: " (vf uid)))
(when parent (hf "parent: " (vf parent)))
(when data (hf " data: " (vf data)))
#_(when kvs (hf " kvs: " (vf kvs))) ; Don't auto include in output
(when ctx (hf " ctx: " (vf ctx))))
(fn signal-content
([signal]
(let [sb (enc/str-builder)
s++ (enc/sb-appender sb nl)]
(signal-content signal s++ enc/pr-edn*)
(str sb)))
(let [{:keys [run-form error]} signal]
(when run-form
(let [{:keys [run-val run-nsecs]} signal
run-time (when run-nsecs (when-let [ff format-nsecs-fn] (ff run-nsecs)))
run-info
(if error
{:form run-form
:time run-time
:nsecs run-nsecs}
;; Undocumented, advanced arity
([signal append-fn val-fn]
(let [af append-fn
vf val-fn]
{:form run-form
:time run-time
:nsecs run-nsecs
:val run-val
#?@(:clj [:val-type (enc/class-sym run-val)])})]
(let [{:keys [uid parent data kvs ctx #?(:clj thread) sample-rate]} signal]
(when sample-rate (af " sample: " (vf sample-rate)))
(when uid (af " uid: " (vf uid)))
(when parent (af " parent: " (vf parent)))
#?(:clj (when (and thread incl-thread?) (af " thread: " (vf thread))))
(when data (af " data: " (vf data)))
(when (and kvs incl-kvs?) (af " kvs: " (vf kvs)))
(when ctx (af " ctx: " (vf ctx))))
(hf " run: " (vf run-info))))
(let [{:keys [run-form error]} signal]
(when run-form
(let [{:keys [run-val run-nsecs]} signal
run-time (when run-nsecs (when-let [ff format-nsecs-fn] (ff run-nsecs)))
run-info
(if error
{:form run-form
:time run-time
:nsecs run-nsecs}
(when error
(if raw-error?
(hf " error: " error)
(when-let [ff format-error-fn]
(hf err-start (ff error) err-stop)))))))))
{:form run-form
:time run-time
:nsecs run-nsecs
:val run-val
#?@(:clj [:val-type (enc/class-sym run-val)])})]
(af " run: " (vf run-info))))
;;;; Signal formatters
(when error
(if raw-error?
(af " error: " error)
(when-let [ff format-error-fn]
(af err-start (ff error) err-stop)))))))))))
(defn format-signal->edn-fn
(comment ((signal-content-fn) (tel/with-signal (tel/event! ::ev-id {:data {:k1 "v1"}}))))
(defn pr-signal-fn
"Experimental, subject to change.
Returns a (fn format->edn [signal]) that:
Returns a (fn pr-signal [signal]) that:
- Takes a Telemere signal.
- Returns edn string of the (minified) signal."
([] (format-signal->edn-fn nil))
([{:keys [pr-edn-fn prep-fn]
- Returns machine-readable serialized string of the (minified) signal.
Options include:
`pr-fn` ∈ #{<unary-fn> :edn :json (Cljs only)}
See arglists for more.
Examples:
(pr-signal-fn :edn {<opts>})
(pr-signal-fn :json {<opts>}) ; Cljs only
;; To output JSON for Clj, you must provide an appropriate `pr-fn`.
;; `jsonista` is a good option, Ref. <https://github.com/metosin/jsonista>:
(require '[jsonista.core :as jsonista])
(pr-signal-fn jsonista/write-value-as-string {<opts>})
See also `format-signal-fn` for human-readable output."
([pr-fn] (pr-signal-fn pr-fn nil))
([pr-fn
{:keys [incl-thread? incl-kvs? incl-newline?, prep-fn]
:or
{pr-edn-fn pr-edn
prep-fn (comp error-in-signal->maps minify-signal)}}]
{incl-newline? true
prep-fn
(comp error-in-signal->maps
minify-signal)}}]
(fn format-signal->edn [signal]
(let [signal* (if prep-fn (prep-fn signal) signal)]
(pr-edn-fn signal*)))))
(let [nl newline
pr-fn
(or
(case pr-fn
:edn pr-edn
#?@(:cljs [:json pr-json])
(comment ((format-signal->edn-fn) {:level :info, :msg "msg"}))
(if (fn? pr-fn)
(do pr-fn)
(enc/unexpected-arg! pr-fn
{:context `pr-signal-fn
:param 'pr-fn
:expected
#?(:clj '#{:edn unary-fn}
:cljs '#{:edn :json unary-fn})}))
(defn format-signal->json-fn
(have fn? pr-fn)))]
(fn pr-signal [signal]
(let [not-map? (not (map? signal))
signal (if (or incl-kvs? not-map?) signal (dissoc signal :kvs))
signal (if (or incl-thread? not-map?) signal (dissoc signal :thread))
signal (if prep-fn (prep-fn signal) signal)
output (pr-fn signal)]
(if incl-newline?
(str output nl)
(do output)))))))
(comment ((pr-signal-fn :edn) (tel/with-signal (tel/event! ::ev-id {:kvs {:k1 "v1"}}))))
(defn format-signal-fn
"Experimental, subject to change.
Returns a (fn format->json [signal]) that:
Returns a (fn format [signal]) that:
- Takes a Telemere signal.
- Returns JSON string of the (minified) signal.
- Returns human-readable formatted string.
(Clj only): An appropriate `:pr-json-fn` MUST be provided."
([] (format-signal->json-fn nil))
([{:keys [pr-json-fn prep-fn]
See also `pr-signal-fn` for machine-readable output."
([] (format-signal-fn nil))
([{:keys [incl-newline? preamble-fn content-fn]
:or
{#?@(:cljs [pr-json-fn pr-json])
prep-fn (comp error-in-signal->maps minify-signal)}}]
{incl-newline? true
preamble-fn (signal-preamble-fn)
content-fn (signal-content-fn)}}]
(when-not pr-json-fn
(throw
(ex-info (str "No `" `format-signal->json-fn "` `:pr-json-fn` was provided") {})))
(let [nl newline]
(fn format-signal [signal]
(let [preamble (when preamble-fn (preamble-fn signal))
content (when content-fn (content-fn signal))]
(fn format-signal->json [signal]
(let [signal* (if prep-fn (prep-fn signal) signal)]
(pr-json-fn signal*)))))
(comment ((format-signal->json-fn) {:level :info, :msg "msg"}))
(defn format-signal->str-fn
"Experimental, subject to change.
Returns a (fn format->str [signal]) that:
- Takes a Telemere signal.
- Returns a formatted string intended for text consoles, etc."
([] (format-signal->str-fn nil))
([{:keys [format-signal->prelude-fn
format-nsecs-fn format-error-fn]
:or
{format-signal->prelude-fn (format-signal->prelude-fn) ; (fn [signal])
format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
format-error-fn (format-error-fn) ; (fn [error])
}}]
(let [signal-content-handler ; (fn [signal hf vf]
(signal-content-handler
{:format-nsecs-fn format-nsecs-fn
:format-error-fn format-error-fn})]
(fn format-signal->str [signal]
(let [sb (enc/str-builder)
s+ (partial enc/sb-append sb)
s++ (partial enc/sb-append sb (str newline " "))]
(when-let [ff format-signal->prelude-fn] (s+ (ff signal))) ; Prelude
(signal-content-handler signal s++ enc/pr-edn*) ; Content
(str sb))))))
(if preamble
(if incl-newline? (str preamble nl content nl) (str preamble nl content))
(if incl-newline? (str content nl) (str content))))))))
(comment
(tel/with-ctx {:c :C}
(println
((format-signal->str-fn)
((format-signal-fn)
(tel/with-signal
(tel/event! ::ev-id
{:user-k1 #{:a :b :c}

View file

@ -229,10 +229,10 @@
(testing "Call middleware"
(let [c (enc/counter)
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}))
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false}))
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}))
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware [(fn [_] "signal-value")]}))]
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))}))
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c))), :allow? false}))
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))}))
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware (fn [_] "signal-value")}))]
[(is (= rv1 0)) (is (sm? sv1 {:m1 1 :m2 2}))
(is (= rv2 3)) (is (nil? sv2))
@ -240,6 +240,10 @@
(is (= rv4 true)) (is (= sv4 "signal-value"))
(is (= @c 7) "3x run + 4x middleware")]))
(testing "Binding conveyance"
(binding [*dynamic-var* :foo]
(is (sm? (with-sig (sig! {:level :info, :data {:dynamic-var *dynamic-var*}})) {:data {:dynamic-var :foo}}))))
#?(:clj
(testing "Printing"
(let [sv1 (with-sig (sig! {:level :info, :run (+ 1 2), :my-k1 :my-v1}))
@ -256,31 +260,31 @@
(let [c (enc/counter)
sv-h1_ (atom nil)
sv-h2_ (atom nil)
wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})
wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})]
wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware (tel/comp-middleware #(assoc % :hm1 (c)) #(assoc % :hm2 (c)))})
wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware (tel/comp-middleware #(assoc % :hm1 (c)) #(assoc % :hm2 (c)))})]
;; Note that call middleware output is cached and shared across all handlers
(binding [impl/*sig-handlers* [wh1 wh2]]
(let [;; 1x run + 4x handler middleware + 2x call middleware = 7x
rv1 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})
rv1 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))})
sv1-h1 @sv-h1_
sv1-h2 @sv-h2_
c1 @c
;; 1x run
rv2 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false})
rv2 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c))), :allow? false})
sv2-h1 @sv-h1_
sv2-h2 @sv-h2_
c2 @c ; 8
;; 1x run + 4x handler middleware + 2x call middleware = 7x
rv3 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})
rv3 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))})
sv3-h1 @sv-h1_
sv3-h2 @sv-h2_
c3 @c ; 15
;; 4x handler middleware
rv4 (sig! {:level :info, :middleware [(fn [_] {:my-sig-val? true})]})
rv4 (sig! {:level :info, :middleware (fn [_] {:my-sig-val? true})})
sv4-h1 @sv-h1_
sv4-h2 @sv-h2_
c4 @c]
@ -317,7 +321,7 @@
(tel/with-handler :hid1
(fn [sv] (force (:data sv)) (reset! sv_ sv))
{:async nil, :error-fn (fn [x] (reset! error_ x)), :rl-error nil,
:middleware [(fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))]}
:middleware (fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))}
[(is (->> (sig! {:level :info, :when (ex1!)}) (throws? :ex-info "Ex1")) "`~filterable-expansion/allow` throws at call")
(is (->> (sig! {:level :info, :inst (ex1!)}) (throws? :ex-info "Ex1")) "`~inst-form` throws at call")
@ -335,7 +339,7 @@
(testing "Throwing call middleware"
(reset-state!)
[(is (true? (sig! {:level :info, :middleware [(fn [_] (ex1!))]})))
[(is (true? (sig! {:level :info, :middleware (fn [_] (ex1!))})))
(is (= @sv_ :nx))
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))])
@ -670,15 +674,16 @@
#?(:clj " Root: clojure.lang.ExceptionInfo - Ex1\n data: {:k1 \"v1\"}\n\nCaused: clojure.lang.ExceptionInfo - Ex2\n data: {:k2 \"v2\"}\n\nRoot stack trace:\n"
:cljs " Root: cljs.core/ExceptionInfo - Ex1\n data: {:k1 \"v1\"}\n\nCaused: cljs.core/ExceptionInfo - Ex2\n data: {:k2 \"v2\"}\n\nRoot stack trace:\n")))
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
prelude ((utils/format-signal->prelude-fn) sig)] ; "2024-06-09T21:15:20.170Z INFO EVENT taoensso.telemere-tests(592,35) ::ev-id"
[(is (enc/str-starts-with? prelude "2024-06-09T21:15:20.170Z INFO EVENT"))
(is (enc/str-ends-with? prelude "::ev-id"))
(is (string? (re-find #"taoensso.telemere-tests\(\d+,\d+\)" prelude)))])
(testing "signal-preamble-fn"
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
preamble ((utils/signal-preamble-fn) sig)] ; "2024-06-09T21:15:20.170Z INFO EVENT taoensso.telemere-tests(592,35) ::ev-id"
[(is (enc/str-starts-with? preamble "2024-06-09T21:15:20.170Z INFO EVENT"))
(is (enc/str-ends-with? preamble "::ev-id"))
(is (string? (re-find #"taoensso.telemere-tests\(\d+,\d+\)" preamble)))]))
(testing "format-signal->edn-fn"
(testing "pr-signal-fn/edn"
(let [sig (update (with-sig (tel/event! ::ev-id {:inst t0})) :inst enc/inst->udt)
sig* (enc/read-edn ((utils/format-signal->edn-fn) sig))]
sig* (enc/read-edn ((tel/pr-signal-fn :edn) sig))]
(is
(enc/submap? sig*
{:schema 1, :kind :event, :id ::ev-id, :level :info,
@ -688,20 +693,20 @@
:column pnat-int?}))))
#?(:cljs
(testing "format-signal->json-fn"
(testing "pr-signal-fn/json"
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
sig* (enc/read-json ((utils/format-signal->json-fn) sig))]
sig* (enc/read-json ((tel/pr-signal-fn :json) sig))]
(is
(enc/submap? sig*
{"schema" 1, "kind" "event", "id" "taoensso.telemere-tests/ev-id",
"level" "info", "ns" "taoensso.telemere-tests",
"level" "info", "ns" "taoensso.telemere-tests",
"inst" t0s
"line" pnat-int?
"column" pnat-int?})))))
(testing "format-signal->str-fn"
(testing "format-signal-fn"
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))]
(is (enc/str-starts-with? ((utils/format-signal->str-fn) sig)
(is (enc/str-starts-with? ((tel/format-signal-fn) sig)
"2024-06-09T21:15:20.170Z INFO EVENT"))))])])
;;;; File handler

View file

@ -216,4 +216,3 @@ Telemere includes extensive internal help docstrings:
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |

View file

@ -28,4 +28,3 @@ For more info see:
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |

View file

@ -2,19 +2,26 @@ Signal handlers process created signals to *do something with them* (analyse the
# Included handlers
The following signal handlers are currently included out-the-box:
A number of signal handlers are included out-the box. Alphabetically:
| Name | Platform | Output target | Output format |
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | Formatted string [1] |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | Formatted string [1] |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signal data [2] |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | Formatted string [1] |
| [`handler:postal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | Clj | Email (via [postal](https://github.com/drewr/postal)) | Formatted string [1] |
| [`handler:carmine`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.carmine#handler:carmine) [0] | Clj | [Redis](https://redis.io/) (via [Carmine](https://www.taoensso.com/carmine)) | Serialized signals [1] |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | String [2] |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | String [2] |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals [3] |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | String [2] |
| [`handler:logstash`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.logstash#handler:logstash) [0] | Clj | [Logstash](https://www.elastic.co/logstash) | TODO |
| [`handler:open-telemetry-logger`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry-logger) | Clj | [OpenTelemetry](https://opentelemetry.io/) [Java client](https://github.com/open-telemetry/opentelemetry-java) | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) |
| [`handler:postal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | Clj | Email (via [postal](https://github.com/drewr/postal)) | String [2] |
| [`handler:slack`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) [0] | Clj | [Slack](https://slack.com/) (via [clj-slack](https://github.com/julienXX/clj-slack)) | String [2] |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | String [2] |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | String [2] |
- \[1] [Configurable](https://cljdoc.org/d/com.taoensso/telemere/1.0.0-beta3/api/taoensso.telemere#help:signal-formatters): human-readable (default), [edn](https://github.com/edn-format/edn), [JSON](https://www.json.org/), etc.
- \[2] For use with browser formatting tools like [cljs-devtools](https://github.com/binaryage/cljs-devtools).
- \[0] Coming soon
- \[1] Uses [Nippy](https://taoensso.com/nippy) to support all Clojure's rich data types
- \[2] [Human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) (default), or [machine-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) ([edn](https://github.com/edn-format/edn), [JSON](https://www.json.org/), etc.).
- \[3] For use with browser formatting tools like [cljs-devtools](https://github.com/binaryage/cljs-devtools).
- See relevant docstrings (links above) for features, usage, etc.
- See section [8-Community](8-Community.md) for more (community-supported) handlers.
- If there's other handlers you'd like to see, feel free to [ping me](https://github.com/taoensso/telemere/issues), or ask on the [`#telemere` Slack channel](https://www.taoensso.com/telemere/slack). It helps to know what people most need!
@ -69,7 +76,7 @@ To instead writes signals as edn:
;; Create console which writes edn
(def my-handler
(t/handler:console
{:format-signal-fn (taoensso.telemere.utils/format-signal->edn-fn)}))
{:output-fn (t/pr-signal-fn :edn)}))
(my-handler my-signal) ; =>
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}
@ -78,18 +85,41 @@ To instead writes signals as edn:
To instead writes signals as JSON:
```clojure
;; Create console which writes JSON
;; Create console which writes signals as JSON
#?(:clj (require '[jsonista.core :as jsonista]))
(def my-handler
(t/handler:console
{:format-signal-fn
(taoensso.telemere.utils/format-signal->json-fn
{:pr-json-fn jsonista.core/write-value-as-string})}))
(my-handler my-signal) ; =>
;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...}
{:output-fn
(t/pr-signal-fn
#?(:cljs :json
:clj jsonista.core/write-value-as-string))}))
```
Note that when writing JSON with Clojure, you *must* specify a `pr-json-fn`. This lets you plug in the JSON serializer of your choice ([jsonista](https://github.com/metosin/jsonista) is my default recommendation).
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).
### Handler-specific per-signal kvs
Telemere includes a handy mechanism for including arbitrary user-level data/opts in individual signals for use by custom middleware and/or handlers.
Any *non-standard* (user) keys you include in your signal constructor opts will automatically be included in created signals, e.g.:
```clojure
(t/with-signal
(t/event! ::my-id
{:my-middleware-data "foo"
:my-handler-data "bar"}))
;; %>
;; {;; User kvs included inline (assoc'd to signal root)
;; :my-middleware-data "foo"
;; :my-handler-data "bar"
;; :kvs ; And also collected together under ":kvs" key
;; {:my-middleware-data "foo"
;; :my-handler-data "bar"}
;; ... }
```
These user-level data/opts are typically NOT included by default in handler output, making them a great way to convey data/opts to custom middleware/handlers.
# Managing handlers

View file

@ -18,11 +18,11 @@ Why release Telemere as a *new library* instead of just updating Timbre?
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.
I've learnt a lot since then, and would write Timbre differently if I were doing it again today. There's many improvements 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.
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.
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.
That eventually grew into Telemere.
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.
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.