Compare commits

...

207 commits

Author SHA1 Message Date
Peter Taoussanis
798e0fddc4 v1.2.1 (2025-12-16)
Some checks failed
Clj tests / tests (17, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (19, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (21, ubuntu-latest) (push) Has been cancelled
Cljs tests / tests (21, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, macOS-latest) (push) Has been cancelled
Graal tests / tests (17, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, windows-latest) (push) Has been cancelled
2025-12-16 10:16:51 +01:00
Peter Taoussanis
6030c472ae [fix] Timbre->Telemere appender: missing error regression
Recent 47af80319d (in Telemere v1.2.0)
accidentally removed errors from Timbre->Telemere signals.
2025-12-16 10:12:23 +01:00
Peter Taoussanis
1d6bdaf7e5 [doc] Housekeeping
Some checks failed
Clj tests / tests (17, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (19, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (21, ubuntu-latest) (push) Has been cancelled
Cljs tests / tests (21, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, macOS-latest) (push) Has been cancelled
Graal tests / tests (17, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, windows-latest) (push) Has been cancelled
2025-12-12 12:13:59 +01:00
Peter Taoussanis
6cc54527f2 v1.2.0 (2025-12-09)
Some checks are pending
Clj tests / tests (17, ubuntu-latest) (push) Waiting to run
Clj tests / tests (19, ubuntu-latest) (push) Waiting to run
Clj tests / tests (21, ubuntu-latest) (push) Waiting to run
Cljs tests / tests (21, ubuntu-latest) (push) Waiting to run
Graal tests / tests (17, macOS-latest) (push) Waiting to run
Graal tests / tests (17, ubuntu-latest) (push) Waiting to run
Graal tests / tests (17, windows-latest) (push) Waiting to run
2025-12-09 17:55:48 +01:00
Peter Taoussanis
56e35f3f58 [nop] Bump deps
Some checks are pending
Clj tests / tests (17, ubuntu-latest) (push) Waiting to run
Clj tests / tests (19, ubuntu-latest) (push) Waiting to run
Clj tests / tests (21, ubuntu-latest) (push) Waiting to run
Cljs tests / tests (21, ubuntu-latest) (push) Waiting to run
Graal tests / tests (17, macOS-latest) (push) Waiting to run
Graal tests / tests (17, ubuntu-latest) (push) Waiting to run
Graal tests / tests (17, windows-latest) (push) Waiting to run
2025-12-09 13:10:11 +01:00
Peter Taoussanis
a6fc4adf6a [new] OpenTelemetry handler: support spans created outside Telemere
Some checks failed
Clj tests / tests (17, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (19, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (21, ubuntu-latest) (push) Has been cancelled
Cljs tests / tests (21, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, macOS-latest) (push) Has been cancelled
Graal tests / tests (17, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, windows-latest) (push) Has been cancelled
BEFORE this commit:

  Telemere captured OpenTelemetry context only when generating
  tracing signals (`trace!`, `spy!`, etc.).

AFTER this commit:

  Telemere always captures OpenTelemetry context when present.

Motivation for the change:

  Telemere users may have spans automatically created by the
  OpenTelemetry Java Agent, or manually created by other libs
  like clj-otel.

  By always capturing the OpenTelemetry context when present,
  this lets even non-tracing Telemere signals (like `log!`)
  include the relevant trace and span IDs for external tooling.

Thanks to @devurandom for suggesting this, and for helping
debug/identify the necessary changes 🙏
2025-12-05 08:03:41 +01:00
Peter Taoussanis
6155713fde [fix] OpenTelemetry handler: add missing line info to output
Previous code was a vestigial leftover from when signals
had {:line <num> :column <num> ...} rather than
{:coords [<line-num> <column-num>] ...}
2025-12-04 21:13:41 +01:00
Peter Taoussanis
a883df3c41 [new] [#68] Add config to skip host and/or thread info 2025-12-04 21:13:41 +01:00
Peter Taoussanis
e6ce33dd4e [mod] SLF4J->Telemere backend: move noisy stuff out of signal data
This is a BREAKING change for the small minority of users that:

  1. Are using the `taoensso.telemere.slf4j` backend, AND
  2. Are using the low-level `:slf4j/args` or `:slf4j/marker-names`
     values in signal `:data`

BEFORE this commit:

  SLF4J signals contain:
  {:data {:slf4j/kvs {...},
          :slf4j/args [...],
          :slf4j/marker-names #{...}},
   ...}.

AFTER this commit:

  SLF4J signals contain:
  {:data {:slf4j/kvs {...}},
   :kvs  {:slf4j/args <Object[]>,
          :slf4j/markers #{...}},
   ...}

  So:
    - [:data :slf4j/marker-names] has moved to [:kvs :slf4j/markers].
    - [:data :slf4j/args]         has moved to [:kvs :slf4j/args],
      and is now an Object[] rather than vector.

Motivation for the change:

  The new behaviour is a more sensible default.

  Basically: anything in `:data` is included by default in output.
  But :slf4j/args are generally anyway already in the signal's formatted
  message, so this ends up just creating duplicate output.

  Likewise markers are generally used more for filtering/xfns than for
  output labelling, so excluding them from default output is sensible.
2025-12-04 21:13:37 +01:00
Peter Taoussanis
cc680b06f5 [mod] Timbre shim API: move noisy :vargs out of signal data
This is a BREAKING change but only relevant for a TINY minority
of users that:

  1. Are using the `taoensso.telemere.timbre` API shim, AND
  2. Are using the low-level `:vargs` value in signal `:data`

BEFORE this commit:

  The taoensso.telemere.timbre shim API produces signals with
  {:data {:vargs [...]}, ...} with `:vargs` a vector of raw args
  provided by Timbre.

AFTER this commit:

  The taoensso.telemere.timbre shim API produces signals with
  {:kvs {:timbre/vargs [...]}, ...} with `:vargs` a vector of raw
  args provided by Timbre.

  I.e. [:data :vargs] has been moved to [:kvs :timbre/vargs].

Motivation for the change:

  The new behaviour is a more sensible default.

  Basically: anything in `:data` is included by default in output.
  But vargs are generally anyway already in the signal's formatted
  message, so this ends up just creating duplicate output.
2025-12-04 11:24:55 +01:00
Peter Taoussanis
47af80319d [mod] [fix] Timbre->Telemere appender: de-duplicate output formatting
BEFORE this commit:

  The Timbre->Telemere appender produced duplicate preamble output
  (timestamp, namespace, etc.). Both Timbre AND Telemere were adding
  preamble info.

AFTER this commit:

  Now ONLY Telemere adds preamble info.
  Timbre's `:output-fn` is ignored.
2025-12-04 11:22:52 +01:00
Peter Taoussanis
b56e1c4529 [mod] [fix] Timbre->Telemere appender: fix callsite coords
There was unfortunately a bug in the Timbre->Telemere appender that
was producing signals with {:line <num> :column <num> ...} keys instead
of the expected {:coords [<line-num> <column-num>] ...} key.

This commit fixes the mistake, which should help fix issues with
any downstream middleware or handlers that expect a `:coords` key.

Unfortunately this fix could break a small minority of users that
have come to expect `:line` and `:column` keys on their Timbre signals
(none of the built-in middleware or handlers do).

Apologies for the trouble!
2025-12-04 11:22:52 +01:00
Peter Taoussanis
8a3ae14f45 [fix] Correctly handle nil :run opt
Before commit: {:run nil} didn't    register  as a tracing signal
After  commit: {:run nil} correctly registers as a tracing signal with `nil` form

nil forms aren't typically useful or used, but can come up by accident
so it's important to handle these correctly.

The `trace!` docstring also promises that the return value will always be
equal to the given input. This didn't hold before.
2025-12-04 11:22:52 +01:00
Peter Taoussanis
917b1b408e [doc] Clarify that signal content is lazy 2025-12-04 11:22:52 +01:00
Peter Taoussanis
125e006753 [nop] Housekeeping 2025-12-04 11:22:52 +01:00
Peter Taoussanis
f7006f31fe [nop] Update CHANGELOG 2025-12-04 11:22:52 +01:00
Peter Taoussanis
c6a71652d7 v1.2.0-SNAPSHOT 2025-12-04 11:22:52 +01:00
Peter Taoussanis
4cc4f45e7c v1.1.0 (2025-08-22) 2025-08-22 10:35:05 +02:00
Peter Taoussanis
ff9e3f4007 [doc] Simplify README
Too much info, too much overwhelm.

Telemere's no more difficult to use than Timbre, let's make that clear.
2025-08-22 10:25:47 +02:00
Peter Taoussanis
eb28d365a8 [fix] truss/ex-info format 2025-08-22 10:25:47 +02:00
Peter Taoussanis
d9ad1ba379 [nop] Bump deps 2025-08-22 10:25:47 +02:00
Peter Taoussanis
b2a8b66cc0 [fix] :trace level JS console logging
Some checks are pending
Clj tests / tests (21, ubuntu-latest) (push) Waiting to run
Clj tests / tests (17, ubuntu-latest) (push) Waiting to run
Clj tests / tests (19, ubuntu-latest) (push) Waiting to run
Cljs tests / tests (21, ubuntu-latest) (push) Waiting to run
Graal tests / tests (17, macOS-latest) (push) Waiting to run
Graal tests / tests (17, ubuntu-latest) (push) Waiting to run
Graal tests / tests (17, windows-latest) (push) Waiting to run
`js/console.trace` was being misused here and is actually intended
for stacktrace printing, NOT trace-level output.
2025-08-21 12:40:39 +02:00
Peter Taoussanis
1bcd46adf3 [doc] Misc housekeeping
Some checks failed
Clj tests / tests (17, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (19, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (21, ubuntu-latest) (push) Has been cancelled
Cljs tests / tests (21, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, macOS-latest) (push) Has been cancelled
Graal tests / tests (17, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, windows-latest) (push) Has been cancelled
2025-06-23 13:10:02 +02:00
Peter Taoussanis
f6ec872f7c [nop] Update project template
Some checks failed
Clj tests / tests (17, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (19, ubuntu-latest) (push) Has been cancelled
Clj tests / tests (21, ubuntu-latest) (push) Has been cancelled
Cljs tests / tests (21, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, macOS-latest) (push) Has been cancelled
Graal tests / tests (17, ubuntu-latest) (push) Has been cancelled
Graal tests / tests (17, windows-latest) (push) Has been cancelled
2025-06-20 11:11:19 +02:00
Peter Taoussanis
b7b3a25a82 [doc] Updates for Trove, simplify examples 2025-06-20 10:53:06 +02:00
Peter Taoussanis
070fe88abb [nop] Update project template 2025-06-19 15:01:41 +02:00
Peter Taoussanis
dcfeba5b91 [new] Add :kvs signal option 2025-06-18 23:17:45 +02:00
Peter Taoussanis
9d655bb9ce [doc] Fix Mulog titles to match its GitHub style 2025-06-18 13:56:33 +02:00
Peter Taoussanis
75a90c6b6d [doc] Emphasize quick examples
Some checks failed
Graal tests / test (17, macOS-latest) (push) Has been cancelled
Graal tests / test (17, ubuntu-latest) (push) Has been cancelled
Graal tests / test (17, windows-latest) (push) Has been cancelled
Main tests / tests (17, ubuntu-latest) (push) Has been cancelled
Main tests / tests (19, ubuntu-latest) (push) Has been cancelled
Main tests / tests (21, ubuntu-latest) (push) Has been cancelled
2025-06-13 11:13:27 +02:00
Peter Taoussanis
269c58d8fe [fix] Clj-kondo warnings for with-signal/s
Some checks failed
Graal tests / test (17, macOS-latest) (push) Has been cancelled
Graal tests / test (17, ubuntu-latest) (push) Has been cancelled
Graal tests / test (17, windows-latest) (push) Has been cancelled
Main tests / tests (17, ubuntu-latest) (push) Has been cancelled
Main tests / tests (19, ubuntu-latest) (push) Has been cancelled
Main tests / tests (21, ubuntu-latest) (push) Has been cancelled
2025-06-09 08:40:15 +02:00
Peter Taoussanis
7603ae2fcf [mod] Tweak format-error spacing
Some checks failed
Graal tests / test (17, macOS-latest) (push) Has been cancelled
Graal tests / test (17, ubuntu-latest) (push) Has been cancelled
Graal tests / test (17, windows-latest) (push) Has been cancelled
Main tests / tests (17, ubuntu-latest) (push) Has been cancelled
Main tests / tests (19, ubuntu-latest) (push) Has been cancelled
Main tests / tests (21, ubuntu-latest) (push) Has been cancelled
2025-05-30 16:28:47 +02:00
Peter Taoussanis
6fb18bd3b9 v1.0.1 (2025-05-27)
Some checks failed
Graal tests / test (17, macOS-latest) (push) Has been cancelled
Graal tests / test (17, ubuntu-latest) (push) Has been cancelled
Graal tests / test (17, windows-latest) (push) Has been cancelled
Main tests / tests (17, ubuntu-latest) (push) Has been cancelled
Main tests / tests (19, ubuntu-latest) (push) Has been cancelled
Main tests / tests (21, ubuntu-latest) (push) Has been cancelled
2025-05-27 09:03:22 +02:00
Peter Taoussanis
d6264afe7c [nop] Bump deps 2025-05-27 08:59:58 +02:00
Peter Taoussanis
f08b60bce4 [fix] [#65] Fix broken callsite :limit option 2025-05-27 08:59:58 +02:00
Peter Taoussanis
3746de8039 [fix] Fix bad signal-content-fn parent formatting 2025-05-27 08:59:58 +02:00
Peter Taoussanis
1bdb667b6c [doc] Add extra docs re: debugging filtering 2025-05-27 08:59:58 +02:00
Mark Sto
2e0a2938b7 [doc] [#64] Hide some unimportant vars from API docs (@marksto)
Motivation:

  These vars aren't used often, are are supplementary rather than
  a key part of Telemere's API.

  Having them listed in the API docs adds to the noise there and makes
  the API seem more overwhelming than necessary.
2025-05-27 08:59:19 +02:00
Mark Sto
9d040d70cd [doc] [#63] Add link to community Axiom handler (@marksto) 2025-05-27 08:59:13 +02:00
Peter Taoussanis
475e5ba6c2 v1.0.0 (2025-04-30) 2025-04-30 16:34:22 +02:00
Peter Taoussanis
51e8a1062f [fix] [#61] OpenTelemetry handler not cancelling timer on shutdown 2025-04-30 14:50:40 +02:00
Peter Taoussanis
31a4fc26d2 [new] Support :host, :thread override 2025-04-30 14:50:40 +02:00
Peter Taoussanis
94fec57c9e [doc] Use consistent style for docstring opts 2025-04-30 14:50:40 +02:00
Peter Taoussanis
345b125f6b [new] Add callsite info to compile-time errors 2025-04-30 14:50:40 +02:00
Peter Taoussanis
248e91f982 [nop] Misc improvements 2025-04-30 14:50:40 +02:00
Peter Taoussanis
32e8909e42 [nop] Move docstring resources 2025-04-30 14:50:40 +02:00
Peter Taoussanis
e8f02ac13e [nop] Use pr-edn* for Signal strings 2025-04-30 14:50:40 +02:00
Peter Taoussanis
254cd6471b [fix] [#32] Fix clj-kondo declaration typo (@icp1994) 2025-04-30 14:50:40 +02:00
Peter Taoussanis
c2e7d0c2d6 [nop] Bump deps 2025-04-30 14:50:40 +02:00
Peter Taoussanis
0608d43d44 v1.0.0-RC5 (2025-03-10) 2025-03-10 13:38:49 +01:00
Peter Taoussanis
d67fc4a76d [doc] Update image: rename "middleware" -> "transform" 2025-03-10 13:02:57 +01:00
Peter Taoussanis
f37f54e1da [mod] Rename "rate-limit" -> "limit"
Users caught by this change should receive a clear compile-time error.

Apologies for the nuissance!! This change is part of a final review
of names before the release of v1 final.
2025-03-10 13:02:57 +01:00
Peter Taoussanis
1f4b49a21a [mod] Rename "sample-rate" -> "sample"
Users caught by this change should receive a clear compile-time error.

Apologies for the nuissance!! This change is part of a final review
of names before the release of v1 final.
2025-03-10 13:02:57 +01:00
Peter Taoussanis
7cccf672f5 [mod] Rename "middleware" -> "transform" (xfn)
Users caught by this change should receive a clear compile-time error.

Apologies for the nuissance!! This change is part of a final review
of names before the release of v1 final.

Motivations:

  - "xfn" is a lot shorter than "middleware", making it more
    convenient to use at signal calls, compare:

    (log! {:middleware my-fn} "msg")
    (log! {:xfn my-fn} "msg"}

  - "middleware" was originally chosen to match Timbre's terminology,
    but actually carries some misleading connotations that in hindsight
    are probably better avoided while we still have the chance to change
    this.
2025-03-10 13:02:57 +01:00
Peter Taoussanis
c78eb07385 [mod] [#56] utils/clean-signal-fn exclude :schema by default
It's probably more common for users to NOT want the `:schema` key
to be included, so let's make that the default.
2025-03-10 13:02:57 +01:00
Peter Taoussanis
82f4c31651 [new] [#57] File handling: make file stream more robust
1. Check `.canWrite` rather than just `.exists`
2. Recreate file and/or dir/s if needed any time
   file stream is recreated.
2025-03-10 13:02:57 +01:00
Peter Taoussanis
af45ffc396 [fix] [#57] File handling: use nio API to create missing parent dirs
Skimming the docs, it seems that the nio API might be better able to
create dirs for edge cases like symbolic links, etc.?
2025-03-10 13:02:57 +01:00
Peter Taoussanis
79173a68cc [fix] [#55] SLF4J signals should include *ctx* 2025-03-10 13:02:57 +01:00
Peter Taoussanis
c60f33edeb [fix] [#32] Fix clj-kondo warnings
Note also related Encore-side PR:
https://github.com/taoensso/encore/pull/84
2025-03-10 13:02:57 +01:00
Peter Taoussanis
bb3d351be8 [nop] Bump deps 2025-03-10 13:02:57 +01:00
Peter Taoussanis
2510c5dbb9 v1.0.0-SNAPSHOT 2025-03-10 13:02:57 +01:00
Peter Taoussanis
9ba4bd986d v1.0.0-RC4 (2025-03-03) 2025-03-03 16:52:44 +01:00
Peter Taoussanis
b03b06de6a [nop] Housekeeping 2025-03-03 16:52:44 +01:00
Peter Taoussanis
6b0e0b9fff [doc] Mention :inst monotonicity 2025-03-03 11:31:52 +01:00
Peter Taoussanis
bfea51570f [new] Alias keep-callsite, mention in signal! docs 2025-03-03 11:30:10 +01:00
Peter Taoussanis
ac5feb4723 [mod] [#53] Breaking: change return value of log!, event!
This change will only affect rare advanced users that depend on
the return value of `log!` or `event!`. For all other users this
will be a non-breaking change.

Before this commit:
  `log!` and `event!` returned true iff signal was allowed.

After this commit:
  `log!`  and `event!` now ALWAYS return nil.
  `log!?` and `event!?` have been added that keep the old behaviour.

Motivation:
  It's pretty rare to use the return value when generating log or event
  signals. I originally included the return value since it CAN be handy,
  and I figured it could just be ignored by those that don't need it.

  But #53 showed that there's a downside I hadn't anticipated - some
  users may actually depend on / prefer a nil return to prevent
  accidentally affecting program flow.

  I think that's a legitimate enough concern to still make a change now
  before v1 final.

  Apologies for the nuissance!
2025-03-03 11:19:36 +01:00
Peter Taoussanis
e32ed8deb5 [new] Add base-opts arg to impl signal creator 2025-03-03 11:01:39 +01:00
Peter Taoussanis
46e82f0816 [nop] Callsite housekeeping 2025-03-03 11:01:39 +01:00
Peter Taoussanis
ea20f6836b [nop] Un-alias public signal-allowed?, signal! macros 2025-03-03 11:01:39 +01:00
Peter Taoussanis
634cc53405 [mod] [#52] signal-preamble-fn should ignore nil :kind (@marksto)
Before this commit: A nil signal `:kind` renders as "DEFAULT".
After  this commit: A nil signal `:kind` isn't rendered at all.

The previous behaviour wasn't particularly useful, and as Mark
helpfully pointed out - makes it difficult to skip `:kind` rendering.

The new behaviour also better matches the behaviour for other signal
keys, which can mostly be dissoc'ed to skip rendering.
2025-03-03 11:01:39 +01:00
Peter Taoussanis
410ed8914c [fix] [#52] signal-preamble-fn should use host info in signal (@marksto) 2025-03-03 11:01:39 +01:00
Peter Taoussanis
78ed4d7f14 [mod] [#51] Make default console handler synchronous by default
This might be less surprising for beginners and new users, so
should probably be the default.
2025-03-03 11:01:39 +01:00
Peter Taoussanis
4fdc55e9b8 [nop] Misc docs houskeeping 2025-03-03 11:01:39 +01:00
Peter Taoussanis
a60f5b8d7c [nop] Bump deps 2025-03-03 11:01:39 +01:00
Peter Taoussanis
dd9f4b2a33 v1.0.0-RC3 (2025-02-27) 2025-02-27 13:04:29 +01:00
Peter Taoussanis
b7d2b4a1ed [nop] Bump deps 2025-02-27 12:45:01 +01:00
Peter Taoussanis
4a6771a907 [doc] Update info on experimental vars 2025-02-27 12:45:01 +01:00
Peter Taoussanis
824f8e3d53 [doc] Rename "signal filters" -> "call filters", etc.
The new terminology hopefully makes the distinction clearer
between call filters and handler filters, etc.
2025-02-27 12:45:01 +01:00
Peter Taoussanis
fda22ce80c [mod] Signal options: drop :location, add :coords
This is the input-side change related to [1], and only
affects folks who've been providing custom callsite info to
Telemere signals (usually in the context of wrapper macros).

To provide custom callsite info BEFORE this commit:
  (tel/signal! {:location {:ns "my-ns", :line 10, :column 20}})

To provide custom callsite info AFTER this commit:
  (tel/signal! {:ns "my-ns", :coords [10 20]})

Motivation for the new override API:

  - It's shorter and cleaner.
  - It's less likely to cause confusion since it avoids the
    redundant signal keys (signals previously contained callsite
    info in 2 duplicate places).
  - The underlying implementation is simpler.
  - The util for manually getting coords is easier to use and doesn't
    require macro-time environment info, making it easier for folks
    to write wrapper macros that include line + column info.
  - When embedded, the new callsite info is shorter and easier for
    Cljs advanced compilation to de-duplicate (so helps reduce .js
    build size).

[1] Commit 1f99f7186b
2025-02-27 12:44:55 +01:00
Peter Taoussanis
1f99f7186b [mod] Signal content: drop :location, add :coords
This is a BREAKING change to get in before v1 final.

Signal keys BEFORE this commit:
  `:location` ---- ?{:keys [ns file line column]} signal creator callsite
  `:ns` ---------- ?str namespace of signal creator callsite, same as (:ns     location)
  `: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)

Signal keys AFTER this commit:
  `:ns` ---------- ?str namespace of signal creator callsite
  `:coords` ------ ?[line column] of signal creator callsite

Motivation for the breaking change:

  The new callsite schema is simpler to use/override, reduces noise, and can reduce
  code expansion size (and so Cljs build size).

  - `:file` was rarely useful, but often added large embedded strings.
  - `:location` was redundant, and often difficult for Closure's
    advanced build to properly de-duplicate.

  This schema will be shared by Truss v2 and Tufte v3.
2025-02-27 12:44:30 +01:00
Peter Taoussanis
bb715fb206 [mod] OpenTelemetry: use standard attribute names when possible 2025-02-26 19:25:23 +01:00
Peter Taoussanis
2c5599c234 [nop] Update to Truss v2 2025-02-26 19:25:23 +01:00
Peter Taoussanis
97efef3d40 [doc] Update signal flow diagram 2025-02-26 17:11:30 +01:00
Peter Taoussanis
2795a6cd52 [nop] Drop enc/pred pattern 2025-02-26 17:11:30 +01:00
Peter Taoussanis
94f13e44f9 [nop] Tweak macro hygiene 2025-02-24 21:56:38 +01:00
Peter Taoussanis
fc7e748ac8 [nop] Stop using optimised binding by default
The improved performance rarely matters in practice, and can
cause issues for folks using deep-walking macros.

Better solution would be to eventually get the optimisation
implemented upstream in Clojure core.
2025-02-24 10:28:24 +01:00
Peter Taoussanis
feb2f64f92 [nop] Drop codox docs 2025-02-20 22:56:30 +01:00
Peter Taoussanis
af14494637 [doc] Misc improvements 2025-02-12 09:18:44 +01:00
Peter Taoussanis
35606d971d [fix] [#45] spy! docstring typo (@rafd) 2025-01-22 14:52:38 +01:00
Peter Taoussanis
7d4aed60d8 [doc] Misc improvements 2025-01-22 14:52:38 +01:00
Peter Taoussanis
f984cdd213 [nop] Simplify dir structure 2025-01-14 10:35:25 +01:00
Peter Taoussanis
db26a5d683 [fix] Fix environment val docs 2025-01-13 23:30:56 +01:00
Vladimir Pouzanov
413cce87c3 [new] [#44] Open Telemetry handler: add span kind option (@farcaller) 2025-01-03 16:19:09 +01:00
lvh
db0498b22c [doc] [#43] Note that ns filters work for SLF4J logger names (@lvh) 2024-12-31 16:16:36 +01:00
Peter Taoussanis
0e642ba21f [fix] Timbre shim: don't attach empty :vargs data 2024-12-31 12:28:22 +01:00
Peter Taoussanis
1517f30abf [doc] [#42] Timbre shim: document different spy error handling 2024-12-31 12:28:22 +01:00
Peter Taoussanis
3a9ffc6206 [fix] [#42] Timbre shim: rename spy! -> spy (@lvh) 2024-12-31 12:17:43 +01:00
Peter Taoussanis
8c7caf45fa v1.0.0-SNAPSHOT 2024-12-31 09:46:02 +01:00
Peter Taoussanis
17dcde97aa v1.0.0-RC2 (2024-12-24) 2024-12-24 11:19:33 +01:00
Peter Taoussanis
a04f255146 [new] Add & opts support to signal!, signal-allowed? 2024-12-24 10:35:11 +01:00
Peter Taoussanis
8cd4ca97e6 [doc] [#33] Add community examples link to Bling Gist 2024-12-24 10:35:07 +01:00
Peter Taoussanis
8412ac29f2 [nop] Tests: remove unnecessary submap pred 2024-12-23 23:04:40 +01:00
Peter Taoussanis
cb6a5d9e1b [mod] NB Change return value of experimental with-signals
The old (deeply-nested) return value seemed to be difficult
for users to read, and prone to mistakes when destructuring.

The new (map) return value is a little more verbose, but
more obvious and less error-prone. Overall a good trade-off
given that this util is anyway used mostly for debugging
or unit tests.
2024-12-23 23:04:40 +01:00
Peter Taoussanis
5c977a348b [doc] Better document pattern of using trace!/spy! with catch->error! 2024-12-23 23:04:40 +01:00
Peter Taoussanis
0de5c094e5 [mod] Remove advanced options from catch->error!
`catch->error!` with default opts is quite handy for use with `trace!`/`spy!`.

But there's a lot that users might want to customize, including:

  - Exactly what error type to catch.
  - Whether or not to rethrow on catch.
  - Error binding sym to enable use within signal message, data, etc.

We could support all of this via `catch->error!` opts but there's not much
point. If anyway customizing such behaviour, it'd be better for the user to just
use an appropriate `try/catch`.

So I've now documented this recommendation, and removed all but the most basic
(:catch-val) options.

This is a BREAKING change for anyone that was previously using any of the
following options:

  :rethrow?
  :catch-sym

Note that `:rethrow?` was never particularly helpful (independently of
`:catch-val` anyway), and the removal of `:catch-sym` will throw a compile-time
error for any existing users.
2024-12-23 23:04:40 +01:00
Peter Taoussanis
d2386d62f1 [new] Refactor impln of common signal creators
Objectives:

  - Support single map opts arg in all cases.

  - Make it easier for folks to inspect the source to understand how
    the common creators use/wrap underlying `signal!`.

Also updated relevant docstrings, etc.
2024-12-23 12:30:00 +01:00
Peter Taoussanis
ace6e2dd2c [new] Add timbre->telemere appender and update docs 2024-12-23 11:19:05 +01:00
Peter Taoussanis
d563ac1259 [new] Better error message when signal! given non-map arg 2024-12-22 13:58:03 +01:00
Peter Taoussanis
f522307ee0 [fix] Trace formatting: always include root info 2024-12-22 13:58:03 +01:00
Peter Taoussanis
68a894edab [fix] Trace formatting: properly format nil ids 2024-12-22 13:58:03 +01:00
Peter Taoussanis
d61f6c25e3 [mod] Remove "- " msg separator from default preamble output
Better to make this optional through the new `:format-msg-fn` option
available for `signal-preamble-fn`
2024-12-22 13:58:03 +01:00
Damiano Ruehl
0822217480 [new] [#34] Add new signal-preamble-fn opts (@Knotschi)
New opts:

  - `:format-id-fn`
  - `:format-msg-fn`

This way its easier to reuse signal-preamble-fn for custom handlers.
If nil is passed as any format fn: value won't be logged.
The `-` char before the msg is now part of the formatter fn.
2024-12-22 13:58:03 +01:00
Peter Taoussanis
9dc9a4645b [new] Alias low-level formatters in utils ns 2024-12-22 13:58:03 +01:00
Peter Taoussanis
096c432eff [mod] [#39] Remove shell API 2024-12-22 13:58:03 +01:00
Peter Taoussanis
706a8b6d37 [mod] Postal handler now uses default preamble fn for email subject 2024-12-22 13:58:03 +01:00
Peter Taoussanis
55323f1f54 [mod] Default signal-content-fn: omit redundant parent/root id namespaces 2024-12-22 13:58:03 +01:00
Peter Taoussanis
b208532788 [mod] Default signal-content-fn: swap ctx, kvs position 2024-12-22 13:58:03 +01:00
Peter Taoussanis
0464285ce1 [mod] Default signal-content-fn: omit :root if it's same as parent 2024-12-22 13:58:03 +01:00
Peter Taoussanis
7eb46ff555 [nop] Misc housekeeping 2024-12-22 13:58:03 +01:00
Peter Taoussanis
cca8bb33ff [doc] Misc improvements 2024-12-22 13:58:03 +01:00
Peter Taoussanis
55720aca54 [doc] [#35] Emphasize that opts need to be a compile-time map 2024-12-22 13:58:03 +01:00
Peter Taoussanis
822032de13 [doc] Add FAQ item re: event! arg order 2024-12-22 13:50:01 +01:00
Peter Taoussanis
13d9dbfc62 [doc] Document that :msg may be a delay 2024-12-22 13:50:01 +01:00
Peter Taoussanis
484b3df122 [new] Improve error info on worst-case handler errors 2024-12-22 13:50:01 +01:00
Peter Taoussanis
7532c2eca5 [new] Give signal! a default kind and level 2024-12-22 13:49:53 +01:00
Peter Taoussanis
9dc883dce9 [new] Allow manual :run-val override
Useful for eliding noisy/long vals from tracing, etc.
2024-12-20 15:49:53 +01:00
Peter Taoussanis
d78663a528 [new] Omit empty :data, :ctx from signal content output 2024-12-20 15:49:53 +01:00
Peter Taoussanis
385c671756 [new] Add private format-location util 2024-12-20 15:49:53 +01:00
Peter Taoussanis
b58ec7359d [fix] [#36] Fix missing cljdoc docstrings
These remote declarations were unnecessary (vestigial), and seemed
to be causing issues with cljdoc's analysis.
2024-12-20 15:49:53 +01:00
Peter Taoussanis
8c701d4df5 [fix] Signal string representation 2024-12-20 14:38:40 +01:00
Peter Taoussanis
70ccfcfd80 [nop] Bump deps 2024-12-20 14:38:40 +01:00
Peter Taoussanis
69e8ed19b8 v1.0.0-RC1 (2024-10-29) 2024-10-29 10:48:41 +01:00
Peter Taoussanis
b5680c5cb7 [nop] Housekeeping 2024-10-29 10:11:13 +01:00
Peter Taoussanis
e1dcdc8257 [doc] Misc improvements 2024-10-29 10:02:13 +01:00
Peter Taoussanis
e60dde03eb [doc] Add community example for GCP (@xlfe) 2024-10-29 10:02:13 +01:00
Peter Taoussanis
5528102f80 [doc] Restructure community examples 2024-10-29 10:02:13 +01:00
Peter Taoussanis
280ad0823f [doc] Collapsible examples in README 2024-10-29 10:02:13 +01:00
Peter Taoussanis
4f5eda0489 [doc] Fix incorrect :msg_ key info 2024-10-29 10:02:13 +01:00
Peter Taoussanis
3d71b70503 [doc] [#27] Typo in link (@blnote) 2024-10-29 10:02:13 +01:00
Peter Taoussanis
2d8c528a6a [doc] [#25] Expand info on IoC tracing, etc. 2024-10-29 10:02:13 +01:00
Peter Taoussanis
c5c8a188c1 [doc] [#25] Add extra info re: async tracing 2024-10-29 10:02:13 +01:00
Peter Taoussanis
0ca48fa6a1 [doc] Misc improvements 2024-10-29 10:02:13 +01:00
Peter Taoussanis
5a8c407528 [new] Add :ctx+, :middleware+ signal options 2024-10-29 10:02:12 +01:00
Peter Taoussanis
c1e1c1e4cc [new] OpenTelemetry handler: try print map vals as EDN 2024-10-29 10:02:12 +01:00
Peter Taoussanis
5ef4f12c6e [new] [#28] OpenTelemetry handler: support custom signal attrs
Thanks to @benalbrecht for assistance on this feature!
2024-10-29 10:02:12 +01:00
Peter Taoussanis
19548d3fac [new] Simplify default OpenTelemetry providers code, expose SDK 2024-10-29 10:02:12 +01:00
Peter Taoussanis
5ac872566a [new] Add dispatch-signal! util 2024-10-29 10:02:12 +01:00
Peter Taoussanis
9965450f5b [new] writeable-file!: resolve sym links, etc. 2024-10-29 10:02:12 +01:00
Peter Taoussanis
d0ad99d528 [new] Extend IIFE-wrap to Clj
The perf hit is negligible, and we can always re-evaluate this choice again
later. In the meantime, let's err on the side of greatest compatibility.
2024-10-29 10:02:12 +01:00
Peter Taoussanis
f7a56631c5 [fix] signal-opts: allow map forms as intended 2024-10-29 10:02:12 +01:00
Peter Taoussanis
7f52cb1843 [fix] uncaught->error! wasn't working (@benalbrecht)
`__thread` handler arg was being masked by `__thread` in signal implementation,
Ref. https://clojurians.slack.com/archives/C06ALA6EEUA/p1727713025725089
2024-10-29 10:02:12 +01:00
Peter Taoussanis
ecf4824f6b [nop] Bump deps 2024-10-29 09:48:36 +01:00
Peter Taoussanis
980439c646 v1.0.0-SNAPSHOT 2024-09-25 09:21:05 +02:00
Peter Taoussanis
0a3e3e80c6 v1.0.0-beta25 (2024-09-25) 2024-09-25 09:14:48 +02:00
Peter Taoussanis
ce9864a57b v1.0.0-SNAPSHOT 2024-09-23 09:23:22 +02:00
Peter Taoussanis
262c6d4324 v1.0.0-beta24 (2024-09-23) 2024-09-23 09:17:49 +02:00
Peter Taoussanis
88f7a3c7d6 [fix] Don't count non-list run forms 2024-09-23 09:14:29 +02:00
Peter Taoussanis
69df7aa86d v1.0.0-SNAPSHOT 2024-09-22 12:30:14 +02:00
Peter Taoussanis
7e348465ac v1.0.0-beta23 (2024-09-22) 2024-09-22 12:20:14 +02:00
Peter Taoussanis
85772f7335 [new] Cap length of displayed run-form when tracing 2024-09-22 10:38:10 +02:00
Peter Taoussanis
c9e84e8b38 [new] Avoid duplicated trace bodies
Trade off a small performance hit with tracing to avoid duplication
of potentially large expansions, and to help further eliminate potential
issues when embedding within IOT-style macros (`core.async/go`, etc.)
2024-09-20 22:55:12 +02:00
Peter Taoussanis
cbab57be66 [fix] [#21] Work around issue with use in Cljs core.async/go bodies
Problem:
  (clojure.core.async/go (taoensso.telemere/log! "hello")) ; Compiles fine
  (cljs.core.async/go    (taoensso.telemere/log! "hello")) ; Compile fails

I could try to get to the bottom of exactly what's going on - but ultimately
IOC mechanisms like `go` are always going to be a bit fragile, especially for
heavily-optimized/unusual code.

In this case, the problem is thankfully only with Cljs - and Telemere's Cljs
performance isn't too critical - so I think we can afford to just bypass any
potential fiddling by the `go` macro by wrapping Cljs Telemere expansions in
an IIFE ((fn [] ...)).

Downside is the (small) added cost of a function construction and call.
Upside   is avoiding potential issues with core.async and other similar
IOC-style systems (Electric Clojure, etc.)
2024-09-20 22:55:12 +02:00
Peter Taoussanis
568906c96b [fix] [#20] Wrong :arglists meta on spy! 2024-09-20 22:55:12 +02:00
Peter Taoussanis
d9c3583631 [new] Add :rate-limit-by option to all signal creators
When present, will cause limits to be per [expansion, by-value]
2024-09-20 22:55:12 +02:00
Peter Taoussanis
f703630914 [mod] Update pr-signal-fn to use clean-signal-fn 2024-09-20 22:55:12 +02:00
Peter Taoussanis
be55f44a87 [new] Add clean-signal-fn util 2024-09-20 22:55:12 +02:00
Peter Taoussanis
d12b0b145b [new] Add signal-allowed? util
Useful for using Telemere's filtering features without generating
any signals, etc.
2024-09-20 22:55:12 +02:00
Peter Taoussanis
a9005e7f1c [mod] Rename taoensso.telemere.api -> taoensso.telemere.shell 2024-09-20 22:55:12 +02:00
Peter Taoussanis
965c2277fd [new] Allow compile-time config of uid kind 2024-09-20 22:55:12 +02:00
Peter Taoussanis
a09c628f23 [nop] Signal fields: revert to initial behaviour
Seems issue with "Electric Clojure" might not be on Telemere's side.
2024-09-20 22:55:12 +02:00
Peter Taoussanis
9b24b54215 [fix] Signal fields: define same fields regardless of platform
Attempted fix for possible issue with "Electric Clojure"
2024-09-20 22:55:12 +02:00
Peter Taoussanis
92a7aee530 [fix] Signal fields: define based on target (not macro) platform
Attempted fix for possible issue with "Electric Clojure"
2024-09-20 22:55:12 +02:00
Peter Taoussanis
f52a04b4dc [fix] [#18] Support {:uid :auto} for non-tracing signal creators 2024-09-20 22:55:12 +02:00
Peter Taoussanis
974df3e152 [doc] Drop suggested edn suffixes from env config 2024-09-20 22:55:12 +02:00
Peter Taoussanis
e4a0a41a1b [doc] Misc improvements 2024-09-20 22:55:12 +02:00
Peter Taoussanis
f9564b2fc5 [nop] Misc improvements 2024-09-20 09:37:28 +02:00
Peter Taoussanis
7e8f692b93 v1.0.0-SNAPSHOT 2024-08-28 18:43:36 +02:00
Peter Taoussanis
97f0eb5efd v1.0.0-beta22 (2024-08-28) 2024-08-28 18:35:25 +02:00
Peter Taoussanis
77ed27cfd1 [mod] Move dep: com.taoensso/slf4j-telemere -> com.taoensso/telemere-slf4j 2024-08-28 18:26:52 +02:00
Peter Taoussanis
ece51b2ef6 [new] Add experimental facade API for lib authors, etc. 2024-08-28 18:26:52 +02:00
Peter Taoussanis
0f09b797ed [fix] More robust mechanism for retaining nested macro callsite info 2024-08-28 16:54:57 +02:00
Peter Taoussanis
0e4942e99c [fix] :line unit tests 2024-08-28 16:54:55 +02:00
Peter Taoussanis
19cd1af3a4 [nop] Restructure repo 2024-08-28 16:53:31 +02:00
Peter Taoussanis
824ebc7802 [doc] Update docs and examples 2024-08-28 09:17:33 +02:00
Peter Taoussanis
96cc9e51f4 [nop] Misc housekeeping 2024-08-27 14:36:32 +02:00
Peter Taoussanis
3388103acf v1.0.0-SNAPSHOT 2024-08-26 22:14:56 +02:00
Peter Taoussanis
6032d2405e v1.0.0-beta21 (2024-08-26) 2024-08-26 13:19:43 +02:00
Peter Taoussanis
3068ccf8d7 [new] Simplify signal expansion 2024-08-26 13:13:33 +02:00
Peter Taoussanis
bbfe61106c [new] Add basic "full" bench numbers (to handled signals) 2024-08-26 13:13:33 +02:00
Peter Taoussanis
b4b06f324b [nop] Micro-optimize binding conveyance
Avoid repeatedly capturing the same callsite binding frame for each registered
handler. While the perf benefit of this change is minimal, the approach is also
conceptually cleaner.
2024-08-26 13:13:33 +02:00
Peter Taoussanis
8066776a80 [doc] Update docs and examples 2024-08-26 09:57:29 +02:00
Peter Taoussanis
54129e91a0 [nop] Misc housekeeping 2024-08-24 11:14:50 +02:00
Peter Taoussanis
9a0bdf92f2 v1.0.0-SNAPSHOT 2024-08-24 11:14:50 +02:00
Peter Taoussanis
b997a3549e v1.0.0-beta20 (2024-08-23) 2024-08-23 14:08:23 +02:00
Peter Taoussanis
ddc9480d20 [doc] Update docs and examples 2024-08-23 14:06:02 +02:00
Peter Taoussanis
84957c6d0a [new] OpenTelemetry handler: improve span interop
When this feature is enabled (see `otel-tracing?`), Telemere's tracing
signal creators (`trace!`, `spy!`, etc.) will now manipulate OpenTelemetry's
span context when relevant.

Before this commit:

  Telemere would detect and use OpenTelemetry span context, but
  the inverse wasn't true: OpenTelemetry instrumentation wouldn't
  recognize Telemere spans.

After this commit:

  Telemere detects OpenTelemetry span context, and the inverse is
  also true: OpenTelemetry instrumentation will recognize Telemere
  spans.

The net effect:

  When you use Telemere to trace forms that may themselves do
  auto/manual OpenTelemetry instrumentation - the resulting spans
  will now properly identify Telemere's spans as parents.

Note that this is interop is implemented in a unique way that retains
Telemere's usual benefits re: low costs at signal callsite, and ability
to skip costs when filtering / sampling / rate-limiting / etc.
2024-08-23 14:06:02 +02:00
Peter Taoussanis
ef678bcc36 [mod] Generalize "intake", rename -> "interop"
Extending feature to cover general interop like OpenTelemetry tracing, etc.
2024-08-23 14:05:46 +02:00
Peter Taoussanis
88eb5211f7 [mod] Make :host output opt-in for default signal handlers 2024-08-23 13:59:22 +02:00
Peter Taoussanis
331bea7a51 [nop] Misc housekeeping 2024-08-22 17:40:17 +02:00
Peter Taoussanis
58b3af893c v1.0.0-SNAPSHOT 2024-08-22 16:20:04 +02:00
Peter Taoussanis
b44eb106a3 v1.0.0-beta19 (2024-08-20) 2024-08-20 19:28:16 +02:00
Peter Taoussanis
064ef32377 [mod] OpenTelemetry handler: rename (generalize)
Handler now does more than just logging.
2024-08-20 19:25:53 +02:00
Peter Taoussanis
a8e92303fa [fix] OpenTelemetry handler: use signal callsite Context as root span parent 2024-08-20 19:25:44 +02:00
Peter Taoussanis
17349a0840 [fix] [#16] OpenTelemetry handler: coerce line attrs (@flyingmachine) 2024-08-20 19:20:56 +02:00
Peter Taoussanis
a1c50f1031 [fix] Decrease min Java version (11->8) (@flyingmachine) 2024-08-20 19:20:56 +02:00
Peter Taoussanis
5b30acc897 [nop] Housekeeping 2024-08-20 18:55:12 +02:00
Peter Taoussanis
c2ad207b18 [doc] New handlers table 2024-08-20 15:14:20 +02:00
Peter Taoussanis
7dc695a18c [doc] Misc improvements 2024-08-20 15:14:20 +02:00
75 changed files with 5181 additions and 4237 deletions

View file

@ -1,4 +1,4 @@
name: Main tests name: Clj tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
@ -7,7 +7,6 @@ jobs:
matrix: matrix:
java: ['17', '19', '21'] java: ['17', '19', '21']
os: [ubuntu-latest] os: [ubuntu-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -15,16 +14,14 @@ jobs:
with: with:
distribution: 'corretto' distribution: 'corretto'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- uses: DeLaGuardo/setup-clojure@12.5 - uses: DeLaGuardo/setup-clojure@12.5
with: with:
lein: latest lein: latest
- uses: actions/cache@v4 - uses: actions/cache@v4
id: cache-deps id: cache-deps
with: with:
path: ~/.m2/repository path: ~/.m2/repository
key: deps-${{ hashFiles('project.clj') }} key: deps-${{ hashFiles('main/project.clj') }}
restore-keys: deps- restore-keys: deps-
- run: lein test-clj
- run: lein test-all working-directory: main

27
.github/workflows/cljs-tests.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Cljs tests
on: [push, pull_request]
jobs:
tests:
strategy:
matrix:
java: ['21']
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: ${{ matrix.java }}
- uses: DeLaGuardo/setup-clojure@12.5
with:
lein: latest
- uses: actions/cache@v4
id: cache-deps
with:
path: ~/.m2/repository
key: deps-${{ hashFiles('main/project.clj') }}
restore-keys: deps-
- run: lein test-cljs
working-directory: main

View file

@ -2,7 +2,7 @@ name: Graal tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test: tests:
strategy: strategy:
matrix: matrix:
java: ['17'] java: ['17']
@ -26,7 +26,13 @@ jobs:
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.m2/repository path: ~/.m2/repository
key: deps-${{ hashFiles('deps.edn') }} key: deps-${{ hashFiles('main/project.clj') }}
restore-keys: deps- restore-keys: deps-
- run: bb graal-tests - name: Run Graal tests
run: bb graal-tests
working-directory: main
# - name: Run Babashka tests
# run: bb bb-tests
# working-directory: main

View file

@ -2,30 +2,248 @@ This project uses [**Break Versioning**](https://www.taoensso.com/break-versioni
--- ---
# `v1.0.0-beta18` (2024-08-19) # `v1.2.1` (2025-12-16)
> **Dep/s**: [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-beta18) and [Telemere SLF4J provider](https://clojars.org/com.taoensso/slf4j-telemere/versions/1.0.0-beta18) are on Clojars. ## 📦 Dependencies
> **Versioning**: Telemere uses [Break Versioning](https://www.taoensso.com/break-versioning).
This is a **pre-release** intended for **early adopters** and those who'd like to give feedback. New betas will be released frequently, while I continue to fix issues and make other improvements/additions. Available on Clojars:
The included handlers and utils are **still undergoing changes**, though the [signal creator](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) and [signal content](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) APIs should already be mostly stable. 1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.2.1) - main dep
2. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.2.1) - extra dep to [send Java logging](https://github.com/taoensso/telemere/wiki/3-Config#java-logging) to Telemere
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! 🙏 This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
This is a **hotfix release** to fix a regression in v1.2.0 that prevented errors from correctly appearing via the Timbre->Telemere appender.
This should be a safe upgrade for users of v1.2.0, apologies for the trouble! - Peter Taoussanis
---
# `v1.2.0` (2025-12-09)
## 📦 Dependencies
Available on Clojars:
1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.2.0) - main dep
2. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.2.0) - extra dep to [send Java logging](https://github.com/taoensso/telemere/wiki/3-Config#java-logging) to Telemere
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
This is a **maintenance and feature release** that should be a safe upgrade for users of v1.1.x, though there have been a few small **changes to signal content** relevant to a very small number of users (see the ➤ items below).
Please **report any unexpected problems** on [GitHub](https://github.com/taoensso/telemere/issues) or the [Slack channel](https://www.taoensso.com/telemere/slack) 🙏 - [Peter Taoussanis](https://www.taoensso.com)
## Since `v1.1.0` (2025-08-22)
- ➤ **\[mod]** SLF4J->Telemere backend: move noisy stuff out of signal data \[e6ce33d]
- ➤ **\[mod]** Timbre shim API: move noisy `:vargs` out of signal data \[cc680b0
- \[mod] [fix] Timbre->Telemere appender: de-duplicate output formatting \[47af803]
- \[mod] [fix] Timbre->Telemere appender: fix callsite coords \[b56e1c4]
- \[fix] OpenTelemetry handler: add missing line info to output \[6155713]
- \[fix] Correctly handle nil `:run` opt \[8a3ae14]
- \[new] OpenTelemetry handler: support spans created outside Telemere \[a6fc4ad]
- \[new] [#68] Add config to skip host and/or thread info \[a883df3]
- \[doc] Clarify that signal content is lazy \[917b1b4]
---
# `v1.1.0` (2025-08-22)
## 📦 Dependencies
Available on Clojars:
1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.1.0) - main dep
2. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.1.0) - extra dep to [send Java logging](https://github.com/taoensso/telemere/wiki/3-Config#java-logging) to Telemere
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
This is a **maintenance release** that fixes a few minor issues, improves docs, and adds some extra API flexibility. It should be a safe upgrade for all users of v1.x.
Please **report any unexpected problems** on [GitHub](https://github.com/taoensso/telemere/issues) or the [Slack channel](https://www.taoensso.com/telemere/slack) 🙏 - [Peter Taoussanis](https://www.taoensso.com)
## Since v1.0.1 (2025-05-27)
- \[fix] `:trace` level JS console logging \[b2a8b66]
- \[fix] Clj-kondo warnings for `with-signal/s` \[269c58d]
- \[new] `with-ctx/+` now takes `& body` instead of a single form (via Encore update)
---
# `v1.0.1` (2025-05-27)
## 📦 Dependencies
Available on Clojars:
1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.1) - main dependency.
2. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.0.1) - additional dependency for users that want their Java logging [to go to](https://github.com/taoensso/telemere/wiki/3-Config#java-logging) Telemere.
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
This is a **hotfix release** that fixes a few issues, and improves some documentation. It should be a safe upgrade for all users of v1.0.0.
## Since `v1` (2025-04-30)
* \[fix] [#65] Fix broken callsite `:limit` option \[f08b60b]
* \[fix] Fix bad `signal-content-fn` parent formatting \[3746de8]
* \[doc] Add extra docs re: debugging filtering \[1bdb667]
* \[doc] [#64] Hide some unimportant vars from API docs (@marksto) \[2e0a293]
* \[doc] [#63] Add link to community Axiom handler (@marksto) \[9d040d7]
---
# `v1.0.0` (2025-04-30)
## 📦 Dependencies
Available on Clojars:
1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0) - main dependency.
2. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.0.0) - additional dependency for users that want their Java logging [to go to](https://github.com/taoensso/telemere/wiki/3-Config#java-logging) Telemere.
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
This is the first stable release of Telemere v1! 🍾🥳🎉
Sincere thanks to everyone that's been helping test and give feedback. As always, please **report any unexpected problems** on [GitHub](https://github.com/taoensso/telemere/issues) or the [Slack channel](https://www.taoensso.com/telemere/slack) 🙏
Telemere is part of a suite of practical and complementary **observability tools** for modern Clojure and ClojureScript applications:
- [Telemere](https://www.taoensso.com/telemere) for logging, tracing, and general telemetry
- [Tufte](https://www.taoensso.com/tufte) for performance monitoring ([v3 RC1 just released](https://github.com/taoensso/tufte/releases/tag/v3.0.0-RC1))
- [Truss](https://www.taoensso.com/truss) for assertions and error handling ([v2 recently released](https://github.com/taoensso/truss/releases/tag/v2.1.0))
New to Telemere? [Start here](https://github.com/taoensso/telemere/wiki/1-Getting-started)!
Upgrading from an earlier version? See the list of changes below👇
Cheers! :-)
\- [Peter Taoussanis](https://www.taoensso.com)
## Changes since `v1 RC1`
> See linked commits for more info:
In **v1 stable** (2025-04-30):
- \[fix] [#61] OpenTelemetry handler not cancelling timer on shutdown \[51e8a10]
- \[fix] [#32] Fix clj-kondo declaration typo (@icp1994) \[254cd64]
- \[new] Support `:host`, `:thread` override \[31a4fc2]
- \[new] Add callsite info to compile-time errors \[345b125]
- \[doc] Use consistent style for docstring opts \[94fec57]
In **v1 RC5** (2025-03-10):
* \[mod] Rename `:rate-limit` -> `:limit` \[f37f54e] (RC5)
* \[mod] Rename `:sample-rate` -> `:sample` \[1f4b49a] (RC5)
* \[mod] Rename `:middleware` -> `:xfn` \[7cccf67] (RC5)
* \[mod] [#56] `utils/clean-signal-fn` exclude `:schema` by default \[c78eb07] (RC5)
* \[fix] [#57] File handling: use nio API to create missing parent dirs \[af45ffc] (RC5)
* \[fix] [#55] SLF4J signals should include `*ctx*` \[79173a6] (RC5)
* \[fix] [#32] Fix clj-kondo warnings \[c60f33e] (RC5)
* \[new] [#57] File handling: make file stream more robust \[82f4c31] (RC5)
In **v1 RC4** (2025-03-03):
* \[mod] `log!`, `event!` now always return nil \[ac5feb4] (RC4)
* \[mod] [#51] Make default console handler sync by default \[78ed4d7] (RC4)
* \[mod] [#52] `signal-preamble-fn` now ignores nil `:kind` (@marksto) \[634cc53] (RC4)
* \[fix] [#52] `signal-preamble-fn` should use host info in signal (@marksto) \[410ed89] (RC4)
* \[new]Add `log!?`, `event!?` \[ac5feb4] (RC4)
* \[new] Alias `keep-callsite`, mention in `signal!` docs \[bfea515] (RC4)
* \[doc][#50] Expand docs for `set-min-level!` (via Encore update) (RC4)
* \[doc] Mention `:inst` monotonicity \[6b0e0b9] (RC4)
In **v1 RC3** (2025-02-27):
* \[mod] Signal content: drop `:location`, add `:coords` \[fda22ce] (RC3)
* \[mod] Signal options: drop `:location`, add `:coords` \[1f99f71] (RC3)
* \[mod] OpenTelemetry: use standard attr names when possible \[bb715fb] (RC3)
* \[fix] Timbre shim: rename `spy!` -> `spy` (@lvh) \[3a9ffc6] (RC3)
* \[fix] Timbre shim: don't attach empty `:vargs` data \[0e642ba] (RC3)
* \[fix] Fix environment val docs \[db26a5d] (RC3)
* \[fix] `spy!` docstring typo (@rafd) \[35606d9] (RC3)
* \[new] Use [Truss](https://www.taoensso.com/truss) v2 and [contextual exceptions](https://cljdoc.org/d/com.taoensso/truss/CURRENT/api/taoensso.truss#ex-info) when relevant (RC3)
* \[new] [#44] Open Telemetry handler: add span kind option (@farcaller) \[413cce8] (RC3)
* \[new] Reduced Cljs build sizes in some cases (RC3)
* \[doc]Timbre shim: document different `spy` error handling \[1517f30] (RC3)
* \[doc] [#43] ns filters work for SLF4J logger names (@lvh) \[db0498b] (RC3)
In **v1 RC2** (2024-12-24):
* \[mod] [#39] Discontinued separate "shell" library \[096c432] (RC2)
* \[mod] Change return value of experimental [`with-signals`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signals) \[cb6a5d9] (RC2)
* \[mod] Remove rarely-used advanced options from [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) \[0de5c09] (RC2)
* \[mod] Remove "- " msg separator from default [preamble](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#signal-preamble-fn) output \[d61f6c2] (RC2)
* \[mod] [Postal handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) now uses default preamble fn for email subject \[706a8b6] (RC2)
* \[mod] Default [`signal-content-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#signal-content-fn): omit redundant parent/root id namespaces \[55323f1] (RC2)
* \[mod] Default [`signal-content-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#signal-content-fn): swap `ctx`, `kvs` position \[b208532] (RC2)
* \[mod] Default [`signal-content-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#signal-content-fn): omit `:root` if it's same as parent \[0464285] (RC2)
* \[mod] Omit empty `:data`, `:ctx` from [signal content](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#signal-content-fn) output \[d78663a] (RC2)
* \[fix] Broken signal string representation \[8c701d4] (RC2)
* \[fix] Trace formatting: always include root info \[f522307] (RC2)
* \[fix] Trace formatting: properly format nil ids \[68a894e] (RC2)
* \[fix] [#36] Fix missing cljdoc docstrings \[b58ec73] (RC2)
* \[new] Add [`timbre->telemere`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.timbre#timbre->telemere-appender) appender and update docs \[ace6e2d] (RC2)
* \[new] All signal creators can now take single opts map \[d2386d6] (RC2)
* \[new] Add `& opts` support to [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!), [`signal-allowed?`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal-allowed?) \[a04f255] (RC2)
* \[new] Give [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) a default kind and level \[7532c2e] (RC2)
* \[new] Better error message when [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) given non-map arg \[d563ac1] (RC2)
* \[new] Improve error info on worst-case handler errors \[484b3df] (RC2)
* \[new] Allow manual `:run-val` override \[9dc883d] (RC2)
* \[new] [#34] Add new [`signal-preamble-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#signal-preamble-fn) opts (@Knotschi) \[0822217] (RC2)
* \[new] Alias low-level formatters in utils ns \[9dc9a46] (RC2)
* \[doc] [#33] Add community examples link to [Bling Gist](https://gist.github.com/ptaoussanis/f8a80f85d3e0f89b307a470ce6e044b5) \[8cd4ca9] (RC2)
* \[doc] Better document pattern of using [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!)/[`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) with [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) \[5c977a3] (RC2)
* \[doc] [#35] Emphasize that [signal opts](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) must be a compile-time map \[55720ac] (RC2)
* \[doc] Add [FAQ item](https://github.com/taoensso/telemere/wiki/6-FAQ#why-the-unusual-arg-order-for-event) re: [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) arg order \[822032d] (RC2)
* \[doc] Document that `:msg` may be a delay \[13d9dbf] (RC2)
---
# `v1.0.0-RC1` (2024-10-29)
## 📦 Dependencies
Available on Clojars:
1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-RC1) - main dep for most users
2. [Shell API](https://clojars.org/com.taoensso/telemere-shell/versions/1.0.0-RC1) - alternative minimal dep [for library authors](https://github.com/taoensso/telemere/wiki/9-Authors#3-telemere-as-an-optional-dependency), etc.
3. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.0.0-RC1) - additional dep for users that want to [interop with Java logging](https://github.com/taoensso/telemere/wiki/3-Config#java-logging)
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
This is the first official **v1 release candidate**. If no unexpected issues come up, this will become **v1 stable**.
As always, 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](https://www.taoensso.com) \- [Peter Taoussanis](https://www.taoensso.com)
## Recent changes ## Recent changes
Latest (beta 18): ### Since `v1.0.0-beta25` (2024-09-25)
* **\[mod]** OpenTelemetry handler: revert #10 \[599236f4] (beta 18) * No API changes
* **\[mod]** Decrease level of :on-init signals \[4d2b5d46] (beta 18)
-- ### Earlier
Earlier:
* \[mod] Update `pr-signal-fn` to use `clean-signal-fn` \[f70363091] (beta 23)
* \[mod] Rename `taoensso.telemere.api` -> `taoensso.telemere.shell` \[a9005e7f1] (beta 23)
* \[mod] Move dep: `com.taoensso/slf4j-telemere` -> [com.taoensso/telemere-slf4j](https://clojars.org/com.taoensso/telemere-slf4j) \[77ed27cfd] (beta 22)
* \[mod] Generalize "intake", rename -> "interop" \[ef678bcc] (beta 20)
* \[mod] Make `:host` output opt-in for default signal handlers \[88eb5211] (beta 20)
* \[mod] OpenTelemetry handler: rename (generalize) \[064ef323] (beta 19)
* \[mod] OpenTelemetry handler: revert #10 \[599236f4] (beta 18)
* \[mod] Decrease level of :on-init signals \[4d2b5d46] (beta 18)
* \[mod] Removed `*auto-stop-handlers?*` var (beta 15) * \[mod] Removed `*auto-stop-handlers?*` var (beta 15)
* \[mod] Removed `:needs-stopping?` [handler dispatch opt](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) (beta 15) * \[mod] Removed `:needs-stopping?` [handler dispatch opt](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) (beta 15)
* \[mod] Cljs handlers MUST now include stop (0) arity (beta 15) * \[mod] Cljs handlers MUST now include stop (0) arity (beta 15)
@ -37,22 +255,39 @@ Earlier:
* \[mod] [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) now takes only a **single opts map** (beta 10) * \[mod] [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) now takes only a **single opts map** (beta 10)
* \[mod] [User-level kvs](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) are **no longer included by default** in handler output. `:incl-kvs?` option has been added to [`format-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#format-signal-fn) and [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) (beta 7) * \[mod] [User-level kvs](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) are **no longer included by default** in handler output. `:incl-kvs?` option has been added to [`format-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#format-signal-fn) and [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) (beta 7)
* \[mod] Middleware must now be a **single fn**, use [`comp-middleware`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#comp-middleware) to create one fn from many (beta 7) * \[mod] Middleware must now be a **single fn**, use [`comp-middleware`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#comp-middleware) to create one fn from many (beta 7)
* \[mod] [OpenTelemetry handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry-logger) is **no longer auto added** (beta 1) * \[mod] [OpenTelemetry handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry) is **no longer auto added** (beta 1)
* \[mod] Various API improvements to [included handlers](https://github.com/taoensso/telemere/wiki/4-Handlers#included-handlers) and [utils](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils) * \[mod] Various API improvements to [included handlers](https://github.com/taoensso/telemere/wiki/4-Handlers#included-handlers) and [utils](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils)
## Recent additions ## Recent additions
Latest (beta 18): ### Since `v1.0.0-beta25` (2024-09-25)
* **\[new]** OpenTelemetry handler: add experimental trace output \[67cb4941] (beta 18) * **\[new]** Add `:ctx+`, `:middleware+` signal options \[5a8c407] (RC1)
* **\[new]** Improve uid control, switch to nano-style by default \[5ab2736c] (beta 18) * **\[new]** OpenTelemetry handler: try print map vals as EDN \[c1e1c1e] (RC1)
* **\[new]** Add host info to signal content \[1cef1957] (beta 18) * **\[new]** [#28] OpenTelemetry handler: support custom signal attrs \[5ef4f12] (RC1)
* **\[new]** Add extra tracing info to signal content \[d635318f] (beta 18) * **\[new]** Simplify default OpenTelemetry providers code, expose SDK \[19548d3] (RC1)
* **\[new]** Add `dispatch-signal!` util \[5ac8725] (RC1)
* **\[new]** `writeable-file!`: resolve sym links, etc. \[9965450] (RC1)
* **\[new]** Extend IIFE-wrap to Clj \[d0ad99d] (RC1)
* **\[new]** Numerous improvements to docs and examples (RC1)
-- ### Earlier
Earlier:
* \[new] Add `:rate-limit-by` option to all signal creators \[d9c358363] (beta 23)
* \[new] Add `clean-signal-fn` util \[be55f44a8] (beta 23)
* \[new] Add `signal-allowed?` util \[d12b0b145] (beta 23)
* \[new] Allow compile-time config of uid kind \[965c2277f] (beta 23)
* \[new] Avoid duplicated trace bodies \[c9e84e8b3] (beta 23)
* \[new] Cap length of displayed run-form when tracing \[85772f733] (beta 23)
* \[new] Added experimental [shell API](https://cljdoc.org/d/com.taoensso/telemere-shell/CURRENT/api/taoensso.telemere.api) for library authors \[ece51b2ef] (beta 22)
* \[new] Auto stop existing handler when replacing it (beta 22)
* \[new] Added `"(.*)"` wildcard syntax to kind/ns/id filters (beta 22)
* \[new] Internal and doc improvements: \[8066776a8], \[b4b06f324], \[3068ccf8d] (beta 21)
* \[new] OpenTelemetry handler: improve span interop \[84957c6d] (beta 20)
* \[new] OpenTelemetry handler: add experimental trace output \[67cb4941] (beta 18)
* \[new] Improve uid control, switch to nano-style by default \[5ab2736c] (beta 18)
* \[new] Add host info to signal content \[1cef1957] (beta 18)
* \[new] Add extra tracing info to signal content \[d635318f] (beta 18)
* \[new] Ongoing [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere) and [wiki](https://github.com/taoensso/telemere/wiki) doc improvements (beta 15) * \[new] Ongoing [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere) and [wiki](https://github.com/taoensso/telemere/wiki) doc improvements (beta 15)
* \[new] [#5] Added [comparison to Mulog](https://github.com/taoensso/telemere/wiki/6-FAQ#how-does-telemere-compare-to-mulog) (beta 15) * \[new] [#5] Added [comparison to Mulog](https://github.com/taoensso/telemere/wiki/6-FAQ#how-does-telemere-compare-to-mulog) (beta 15)
* \[new] SLF4J and `tools.logging` signals now have a namespace (from logger name) (beta 14) * \[new] SLF4J and `tools.logging` signals now have a namespace (from logger name) (beta 14)
@ -71,14 +306,141 @@ Earlier:
## Recent fixes ## Recent fixes
Latest (beta 18): ### Since `v1.0.0-beta25` (2024-09-25)
* No fixes * **\[fix]** `signal-opts`: allow map forms as intended \[f7a5663] (RC1)
* **\[fix]** `uncaught->error!` wasn't working (@benalbrecht) \[7f52cb1] (RC1)
-- ### Earlier
Earlier:
* \[fix] Regression affecting deprecated `rate-limiter*` (beta 25)
* \[fix] Don't try count non-list tracing bodies \[88f7a3c7d] (beta 24)
* \[fix] [#21] Work around issue with use in Cljs `core.async/go` bodies \[cbab57be6] (beta 23)
* \[fix] [#20] Wrong :arglists meta on `spy!` \[568906c96] (beta 23)
* \[fix] [#18] Support `{:uid :auto}` for non-tracing signal creators \[f52a04b4d] (beta 23)
* \[fix] Runtime Clj env config now works correctly in uberjars (beta 23)
* \[fix] Signal `:line` info missing for some wrapped-macro cases \[0f09b797e] (beta 22)
* \[fix] OpenTelemetry handler: use signal callsite Context as root span parent \[a8e92303] (beta 19)
* \[fix] [#16] OpenTelemetry handler: coerce line attrs (@flyingmachine) \[17349a08] (beta 19)
* \[fix] Decrease min Java version (11->8) (@flyingmachine) \[a1c50f10] (beta 19)
* \[fix] Broken handler ns and kind filters \[23194238] (beta 16)
* \[fix] [#10] OpenTelemetry handler: render keywords as plain strings \[6e94215e] (beta 15)
* \[fix] [#11] OpenTelemetry handler: signals without message fail \[863cea15] (beta 15)
* \[fix] [#14] File handler: Don't truncate gzip output \[2d4b0497] (beta 15)
* \[fix] Don't drop signals while draining async buffer during shutdown, add tests (via Encore) (beta 12, beta 13)
* \[fix] [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) wasn't realizing delayed messages, add tests \[cf72017a] (beta 11)
* \[fix] [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) broken custom pr-fn support, add tests \[e7cce0c1] (beta 10)
* \[fix] [#6] Missing root stack trace, add tests \[213c6470] (beta 9)
* \[fix] Broken AOT support, add tests \[ffea1a30] (beta 1)
* \[fix] SLF4J broken timestamps, add tests \[e222297a] (beta 1)
---
# `v1.0.0-beta25` (2024-09-25)
## 📦 Dependencies
Available on Clojars:
1. [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-beta25) - main dep for most users
2. [Shell API](https://clojars.org/com.taoensso/telemere-shell/versions/1.0.0-beta25) - alternative minimal dep [for library authors](https://github.com/taoensso/telemere/wiki/9-Authors#3-telemere-as-an-optional-dependency), etc.
3. [SLF4J provider](https://clojars.org/com.taoensso/telemere-slf4j/versions/1.0.0-beta25) - additional dep for users that want to [interop with Java logging](https://github.com/taoensso/telemere/wiki/3-Config#java-logging)
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
## Release notes
- This is a **pre-release** intended for **early adopters** and those who'd like to give feedback.
- This is expected to be the **last beta** before RC1.
- 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](https://www.taoensso.com)
## Recent changes
### Beta 25, 24, 23
* **\[mod]** Update `pr-signal-fn` to use `clean-signal-fn` \[f70363091] (beta 23)
* **\[mod]** Rename `taoensso.telemere.api` -> `taoensso.telemere.shell` \[a9005e7f1] (beta 23)
### Earlier
* \[mod] Move dep: `com.taoensso/slf4j-telemere` -> [com.taoensso/telemere-slf4j](https://clojars.org/com.taoensso/telemere-slf4j) \[77ed27cfd] (beta 22)
* \[mod] Generalize "intake", rename -> "interop" \[ef678bcc] (beta 20)
* \[mod] Make `:host` output opt-in for default signal handlers \[88eb5211] (beta 20)
* \[mod] OpenTelemetry handler: rename (generalize) \[064ef323] (beta 19)
* \[mod] OpenTelemetry handler: revert #10 \[599236f4] (beta 18)
* \[mod] Decrease level of :on-init signals \[4d2b5d46] (beta 18)
* \[mod] Removed `*auto-stop-handlers?*` var (beta 15)
* \[mod] Removed `:needs-stopping?` [handler dispatch opt](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) (beta 15)
* \[mod] Cljs handlers MUST now include stop (0) arity (beta 15)
* \[mod] Users MUST now **manually call** [`stop-handlers!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#stop-handlers!) (beta 15)
* \[mod] SLF4J and `tools.logging` signals now have a custom `:kind` and no `:id` (beta 14)
* \[mod] Renamed `get-min-level` -> [`get-min-levels`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-min-levels) (beta 13)
* \[mod] Renamed `shut-down-handlers!` -> [`stop-handlers!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#stop-handlers!) (beta 13)
* \[mod] Changed default **handler back-pressure** mechanism from `:dropping` to `:blocking` (eaiser for most users to understand and detect; override when calling [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!)) (beta 11)
* \[mod] [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) now takes only a **single opts map** (beta 10)
* \[mod] [User-level kvs](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) are **no longer included by default** in handler output. `:incl-kvs?` option has been added to [`format-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#format-signal-fn) and [`pr-signal-fn`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils#pr-signal-fn) (beta 7)
* \[mod] Middleware must now be a **single fn**, use [`comp-middleware`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#comp-middleware) to create one fn from many (beta 7)
* \[mod] [OpenTelemetry handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry) is **no longer auto added** (beta 1)
* \[mod] Various API improvements to [included handlers](https://github.com/taoensso/telemere/wiki/4-Handlers#included-handlers) and [utils](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.utils)
## Recent additions
### Beta 25, 24, 23
* **\[new]** Add `:rate-limit-by` option to all signal creators \[d9c358363] (beta 23)
* **\[new]** Add `clean-signal-fn` util \[be55f44a8] (beta 23)
* **\[new]** Add `signal-allowed?` util \[d12b0b145] (beta 23)
* **\[new]** Allow compile-time config of uid kind \[965c2277f] (beta 23)
* **\[new]** Avoid duplicated trace bodies \[c9e84e8b3] (beta 23)
* **\[new]** Cap length of displayed run-form when tracing \[85772f733] (beta 23)
* Updated [Encore](https://www.taoensso.com/encore) to v3.120.0 (2024-09-22) (beta 23)
### Earlier
* \[new] Added experimental [shell API](https://cljdoc.org/d/com.taoensso/telemere-shell/CURRENT/api/taoensso.telemere.api) for library authors \[ece51b2ef] (beta 22)
* \[new] Auto stop existing handler when replacing it (beta 22)
* \[new] Added `"(.*)"` wildcard syntax to kind/ns/id filters (beta 22)
* \[new] Internal and doc improvements: \[8066776a8], \[b4b06f324], \[3068ccf8d] (beta 21)
* \[new] OpenTelemetry handler: improve span interop \[84957c6d] (beta 20)
* \[new] OpenTelemetry handler: add experimental trace output \[67cb4941] (beta 18)
* \[new] Improve uid control, switch to nano-style by default \[5ab2736c] (beta 18)
* \[new] Add host info to signal content \[1cef1957] (beta 18)
* \[new] Add extra tracing info to signal content \[d635318f] (beta 18)
* \[new] Ongoing [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere) and [wiki](https://github.com/taoensso/telemere/wiki) doc improvements (beta 15)
* \[new] [#5] Added [comparison to Mulog](https://github.com/taoensso/telemere/wiki/6-FAQ#how-does-telemere-compare-to-mulog) (beta 15)
* \[new] SLF4J and `tools.logging` signals now have a namespace (from logger name) (beta 14)
* \[new] Added [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) (beta 13)
* \[new] [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) can now specify per-handler `:drain-msecs` (beta 13)
* \[new] Added [`*auto-stop-handlers?*`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#*auto-stop-handlers?*) (beta 13)
* \[new] [`remove-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#remove-handler!) now auto stops relevant handlers after removal (beta 13)
* \[new] [`with-handler`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-handler) and [`with-handler+`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-handler+) now auto stops relevant handlers after use (beta 12)
* \[new] (Advanced) Handler fns can now include `:dispatch-opts` metadata, useful for handler authors that want to set defaults for use by [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) (beta 8)
* \[new] Added [Slack handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) (beta 8)
* \[new] Added [TCP](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) and [UDP](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) socket handlers (beta 7)
* \[new] Clj [signal content](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) now includes `:thread {:keys [group name id]}` key (beta 7)
* \[new] Added [postal (email) handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) (beta 5)
* \[new] Handlers now block to try drain their signal queues on shutdown (beta 3)
* \[new] Rate limiter performance improvements (via Encore) (beta 3)
## Recent fixes
### Beta 25, 24, 23
* **\[fix]** Regression affecting deprecated `rate-limiter*` (beta 25)
* **\[fix]** Don't try count non-list tracing bodies \[88f7a3c7d] (beta 24)
* **\[fix]** [#21] Work around issue with use in Cljs `core.async/go` bodies \[cbab57be6] (beta 23)
* **\[fix]** [#20] Wrong :arglists meta on `spy!` \[568906c96] (beta 23)
* **\[fix]** [#18] Support `{:uid :auto}` for non-tracing signal creators \[f52a04b4d] (beta 23)
* **\[fix]** Runtime Clj env config now works correctly in uberjars (beta 23)
### Earlier
* \[fix] Signal `:line` info missing for some wrapped-macro cases \[0f09b797e] (beta 22)
* \[fix] OpenTelemetry handler: use signal callsite Context as root span parent \[a8e92303] (beta 19)
* \[fix] [#16] OpenTelemetry handler: coerce line attrs (@flyingmachine) \[17349a08] (beta 19)
* \[fix] Decrease min Java version (11->8) (@flyingmachine) \[a1c50f10] (beta 19)
* \[fix] Broken handler ns and kind filters \[23194238] (beta 16) * \[fix] Broken handler ns and kind filters \[23194238] (beta 16)
* \[fix] [#10] OpenTelemetry handler: render keywords as plain strings \[6e94215e] (beta 15) * \[fix] [#10] OpenTelemetry handler: render keywords as plain strings \[6e94215e] (beta 15)
* \[fix] [#11] OpenTelemetry handler: signals without message fail \[863cea15] (beta 15) * \[fix] [#11] OpenTelemetry handler: signals without message fail \[863cea15] (beta 15)

441
README.md
View file

@ -1,248 +1,329 @@
<a href="https://www.taoensso.com/clojure" title="More stuff by @ptaoussanis at www.taoensso.com"><img src="https://www.taoensso.com/open-source.png" alt="Taoensso open source" width="340"/></a> <a href="https://www.taoensso.com/clojure" title="More stuff by @ptaoussanis at www.taoensso.com"><img src="https://www.taoensso.com/open-source.png" alt="Taoensso open source" width="340"/></a>
[**API**][cljdoc docs] | [**Wiki**][GitHub wiki] | [Latest releases](#latest-releases) | [Slack channel][] [**API**][cljdoc] | [**Wiki**][GitHub wiki] | [Slack][] | Latest release: [v1.2.1](../../releases/tag/v1.2.1) (2025-12-16)
[![Clj tests][Clj tests SVG]][Clj tests URL]
[![Cljs tests][Cljs tests SVG]][Cljs tests URL]
[![Graal tests][Graal tests SVG]][Graal tests URL]
# <img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/telemere-logo.svg" alt="Telemere logo" width="360"/> # <img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/telemere-logo.svg" alt="Telemere logo" width="360"/>
### Structured telemetry library for Clojure/Script ### Structured logs and telemetry for Clojure/Script
**Telemere** is a next-generation replacement for [Timbre](https://www.taoensso.com/timbre) that offers a simple **unified API** for **structured and traditional logging**, **tracing**, and **basic performance monitoring**. **Telemere** is the next-gen version of [Timbre](https://www.taoensso.com/timbre). It offers **one API** to cover:
It helps enable Clojure/Script systems that are **observable**, **robust**, and **debuggable** - and it represents the refinement and culmination of ideas brewing over 12+ years in [Timbre](https://www.taoensso.com/timbre), [Tufte](https://www.taoensso.com/tufte), [Truss](https://www.taoensso.com/truss), etc. - **Traditional logging** (string messages)
- **Structured logging** (rich Clojure data types and structures)
- **Tracing** (nested flow tracking, with optional data)
- Basic **performance monitoring** (nested form runtimes)
> [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. It's pure Clj/s, small, **easy to use**, super fast, and **seriously flexible**:
## Latest release/s ```clojure
(tel/log! {:level :info, :id ::login, :data {:user-id 1234}, :msg "User logged in!"})
```
- `2024-08-19` `v1.0.0-beta18`: [release info](../../releases/tag/v1.0.0-beta17) (for early adopters/feedback) Works great with:
[![Main tests][Main tests SVG]][Main tests URL] - [Trove](https://www.taoensso.com/trove) for logging by **library authors**
[![Graal tests][Graal tests SVG]][Graal tests URL] - [Tufte](https://www.taoensso.com/tufte) for rich **performance monitoring**
- [Truss](https://www.taoensso.com/truss) for **assertions** and error handling
See [here][GitHub releases] for earlier releases. ## Why structured logging?
- Traditional logging outputs **strings** (messages).
- Structured logging in contrast outputs **data**. It retains **rich data types and (nested) structures** throughout the logging pipeline from logging callsite → filters → middleware → handlers.
A data-oriented pipeline can make a huge difference - supporting **easier filtering**, **transformation**, and **analysis**. Its also usually **faster**, since you only pay for serialization if/when you need it. In a lot of cases you can avoid serialization altogether if your final target (DB, etc.) supports the relevant types.
The structured (data-oriented) approach is inherently more flexible, faster, and well suited to the tools and idioms offered by Clojure and ClojureScript.
## Examples
See [examples.cljc](https://github.com/taoensso/telemere/blob/master/examples.cljc) for REPL-ready snippets, or expand below:
<details><summary>Create signals</summary><br/>
```clojure
(require '[taoensso.telemere :as tel])
;; No config needed for typical use cases!!
;; Signals print to console by default for both Clj and Cljs
;; Traditional style logging (data formatted into message string):
(tel/log! {:level :info, :msg (str "User " 1234 " logged in!")})
;; Modern/structured style logging (explicit id and data)
(tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}})
;; Mixed style (explicit id and data, with message string)
(tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}, :msg "User logged in!"})
;; Trace (can interop with OpenTelemetry)
;; Tracks form runtime, return value, and (nested) parent tree
(tel/trace! {:id ::my-id :data {...}}
(do-some-work))
;; Check resulting signal content for debug/tests
(tel/with-signal (tel/log! {...})) ; => {:keys [ns level id data msg_ ...]}
;; Getting fancy (all costs are conditional!)
(tel/log!
{:level :debug
:sample 0.75 ; 75% sampling (noop 25% of the time)
:when (my-conditional)
:limit {"1 per sec" [1 1000]
"5 per min" [5 60000]} ; Rate limit
:limit-by my-user-ip-address ; Rate limit scope
:do (inc-my-metric!)
:let
[diagnostics (my-expensive-diagnostics)
formatted (my-expensive-format diagnostics)]
:data
{:diagnostics diagnostics
:formatted formatted
:local-state *my-dynamic-context*}}
;; Message string or vector to join as string
["Something interesting happened!" formatted])
```
</details>
<details><summary>Filter signals</summary><br/>
```clojure
;; Set minimum level
(tel/set-min-level! :warn) ; For all signals
(tel/set-min-level! :log :debug) ; For `log!` signals specifically
;; Set id and namespace filters
(tel/set-id-filter! {:allow #{::my-particular-id "my-app/*"}})
(tel/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"})
;; SLF4J signals will have their `:ns` key set to the logger's name
;; (typically a source class)
(tel/set-ns-filter! {:disallow "com.noisy.java.package.*"})
;; Set minimum level for `log!` signals for particular ns pattern
(tel/set-min-level! :log "taoensso.sente.*" :warn)
;; Use transforms (xfns) to filter and/or arbitrarily modify signals
;; by signal data/content/etc.
(tel/set-xfn!
(fn [signal]
(if (-> signal :data :skip-me?)
nil ; Filter signal (don't handle)
(assoc signal :transformed? true))))
(tel/with-signal (tel/log! {... :data {:skip-me? true}})) ; => nil
(tel/with-signal (tel/log! {... :data {:skip-me? false}})) ; => {...}
;; See `tel/help:filters` docstring for more filtering options
```
</details>
<details><summary>Add handlers</summary><br/>
```clojure
;; Add your own signal handler
(tel/add-handler! :my-handler
(fn
([signal] (println signal))
([] (println "Handler has shut down"))))
;; Use `add-handler!` to set handler-level filtering and back-pressure
(tel/add-handler! :my-handler
(fn
([signal] (println signal))
([] (println "Handler has shut down")))
{:async {:mode :dropping, :buffer-size 1024, :n-threads 1}
:priority 100
:sample 0.5
:min-level :info
:ns-filter {:disallow "taoensso.*"}
:limit {"1 per sec" [1 1000]}
;; See `tel/help:handler-dispatch-options` for more
})
;; See current handlers
(tel/get-handlers) ; => {<handler-id> {:keys [handler-fn handler-stats_ dispatch-opts]}}
;; Add console handler to print signals as human-readable text
(tel/add-handler! :my-handler
(tel/handler:console
{:output-fn (tel/format-signal-fn {})}))
;; Add console handler to print signals as edn
(tel/add-handler! :my-handler
(tel/handler:console
{:output-fn (tel/pr-signal-fn {:pr-fn :edn})}))
;; Add console handler to print signals as JSON
;; Ref. <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista]))
(tel/add-handler! :my-handler
(tel/handler:console
{:output-fn
#?(:cljs :json ; Use js/JSON.stringify
:clj jsonista/write-value-as-string)}))
```
</details>
## Why Telemere? ## Why Telemere?
#### Ergonomics ### Ergonomics
- Elegant, lightweight API that's **easy to use**, **easy to configure**, and **deeply flexible**. - Elegant unified API that's **easy to use** and **deeply flexible**.
- **Sensible defaults** to make getting started **fast and easy**. - Pure **Clojure vals and fns** for easy config, composition, and REPL debugging.
- Extensive **beginner-oriented** [documentation][GitHub wiki], [docstrings](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso), and error messages. - **Sensible defaults** to get started fast.
- **Beginner-oriented** [documentation][GitHub wiki], [docstrings](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere), and error messages.
#### Interop ### Interop
- 1st-class **out-the-box interop** with [SLF4J v2](https://www.slf4j.org/), [tools.logging](https://github.com/clojure/tools.logging), [OpenTelemetry](https://opentelemetry.io/), and [Tufte](https://www.taoensso.com/tufte). - **Interop ready** with [tools.logging](../../wiki/3-Config#toolslogging), [Java logging via SLF4J v2](../../wiki/3-Config#java-logging), [OpenTelemetry](../../wiki/3-Config#opentelemetry), and [Tufte](../../wiki/3-Config#tufte).
- Included [shim](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.timbre) for easy/gradual [migration from Timbre](../../wiki/5-Migrating). - [Timbre shim](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.timbre) for easy/gradual [migration from Timbre](../../wiki/5-Migrating).
- Extensive set of [handlers](../../wiki/4-Handlers#included-handlers) included out-the-box. - Extensive set of [handlers](../../wiki/4-Handlers#included-handlers) included out-the-box.
#### Scaling ### Scaling
- Hyper-optimized and **blazing fast**, see [benchmarks](#benchmarks). - Rich [filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) by namespace, id pattern, level, level by namespace pattern, etc.
- An API that **scales comfortably** from the smallest disposable code, to the most massive and complex real-world production environments. - Fully [configurable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) **a/sync dispatch support** with per-handler [performance monitoring](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats).
- Auto [handler stats](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) for debugging performance and other issues at scale. - Turn-key **sampling**, **rate limiting**, and **back-pressure monitoring**.
- Highly optimized and [blazing fast](#performance)!
#### Flexibility ## Comparisons
- Config via plain **Clojure vals and fns** for easy customization, composition, and REPL debugging. - Telemere [compared](../../wiki/5-Migrating#from-timbre) to [Timbre](https://www.taoensso.com/timbre) (Telemere's predecessor)
- Unmatched [environmental config](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:environmental-config) support: JVM properties, environment variables, or classpath resources. Per platform, or cross-platform. - Telemere [compared](../../wiki/6-FAQ#how-does-telemere-compare-to-%CE%BClog) to [μ/log](https://github.com/BrunoBonacci/mulog) (structured micro-logging library)
- Unmatched [filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) support: by namespace, id pattern, level, level by namespace pattern, etc. At runtime and compile-time.
- Fully [configurable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) **a/sync dispatch support**: blocking, dropping, sliding, etc.
- Turn-key **sampling**, **rate-limiting**, and **back-pressure monitoring** with sensible defaults.
#### Comparisons ## Videos
- Telemere [compared](../../wiki/5-Migrating#from-timbre) to [Timbre](https://www.taoensso.com/timbre) ### Lightning intro (7 mins):
- Telemere [compared](../../wiki/6-FAQ#how-does-telemere-compare-to-mulog) to [Mulog](https://github.com/BrunoBonacci/mulog)
## Video demo <a href="https://www.youtube.com/watch?v=lOaZ0SgPVu4" target="_blank">
<img src="https://img.youtube.com/vi/lOaZ0SgPVu4/maxresdefault.jpg" alt="Telemere lightning intro" width="480" border="0" />
</a>
See for intro and basic usage: ### REPL demo (24 mins):
<a href="https://www.youtube.com/watch?v=-L9irDG8ysM" target="_blank"> <a href="https://www.youtube.com/watch?v=-L9irDG8ysM" target="_blank">
<img src="https://img.youtube.com/vi/-L9irDG8ysM/maxresdefault.jpg" alt="Telemere demo video" width="480" border="0" /> <img src="https://img.youtube.com/vi/-L9irDG8ysM/maxresdefault.jpg" alt="Telemere demo video" width="480" border="0" />
</a> </a>
## Quick examples
```clojure
(require '[taoensso.telemere :as t])
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message") %>
;; 2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message
;; data: {:x1 :x2}
(t/log! "This will send a `:log` signal to the Clj/s console")
(t/log! :info "This will do the same, but only when the current level is >= `:info`")
;; Easily construct messages from parts
(t/log! :info ["Here's a" "joined" "message!"])
;; Attach an id
(t/log! {:level :info, :id ::my-id} "This signal has an id")
;; Attach arb user data
(t/log! {:level :info, :data {:x :y}} "This signal has structured data")
;; Capture for debug/testing
(t/with-signal (t/log! "This will be captured"))
;; => {:keys [location level id data msg_ ...]}
;; `:let` bindings are available to `:data` and message, but only paid
;; for when allowed by minimum level and other filtering criteria
(t/log!
{:level :info
:let [expensive-metric1 (last (for [x (range 100), y (range 100)] (* x y)))]
:data {:metric1 expensive-metric1}}
["Message with metric value:" expensive-metric1])
;; With sampling 50% and 1/sec rate limiting
(t/log!
{:sample-rate 0.5
:rate-limit {"1 per sec" [1 1000]}}
"This signal will be sampled and rate limited")
;; There are several signal creators available for convenience.
;; All support the same options but each offer's a calling API
;; optimized for a particular use case. Compare:
;; `log!` - [msg] or [level-or-opts msg]
(t/with-signal (t/log! {:level :info, :id ::my-id} "Hi!"))
;; `event!` - [id] or [id level-or-opts]
(t/with-signal (t/event! ::my-id {:level :info, :msg "Hi!"}))
;; `signal!` - [opts]
(t/with-signal (t/signal! {:level :info, :id ::my-id, :msg "Hi!"}))
;; See `t/help:signal-creators` docstring for more
;;; A quick taste of filtering
(t/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"})
(t/set-id-filter! {:allow #{::my-particular-id "my-app/*"}})
(t/set-min-level! :warn) ; Set minimum level for all signals
(t/set-min-level! :log :debug) ; Set minimul level for `log!` signals
;; Set minimum level for `event!` signals originating in
;; the "taoensso.sente.*" ns
(t/set-min-level! :event "taoensso.sente.*" :warn)
;; See `t/help:filters` docstring for more
;;; Use middleware to transform signals and/or filter signals
;;; by signal data/content/etc.
(t/set-middleware!
(fn [signal]
(if (get-in signal [:data :hide-me?])
nil ; Suppress signal (don't handle)
(assoc signal :passed-through-middleware? true))))
(t/with-signal (t/event! ::my-id {:data {:hide-me? true}})) ; => nil
(t/with-signal (t/event! ::my-id {:data {:hide-me? false}})) ; => {...}
```
See [examples.cljc](https://github.com/taoensso/telemere/blob/master/examples.cljc) for more REPL-ready snippets!
## API overview ## API overview
See relevant docstrings (links below) for usage info-
### Creating signals ### Creating signals
| Name | Signal kind | Main arg | Optional arg | Returns | 80% of Telemere's functionality is available through one macro: [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) and a rich set of [opts](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options).
| :---------------------------------------------------------------------------------------------------------- | :---------- | :------- | :------------- | :--------------------------- |
| [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) | `:log` | `msg` | `opts`/`level` | Signal allowed? | Use that directly, or any of the wrapper macros that you find most convenient. They're **semantically equivalent** but have ergonomics slightly tweaked for different common use cases:
| [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) | `:event` | `id` | `opts`/`level` | Signal allowed? |
| [`error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error!) | `:error` | `error` | `opts`/`id` | Given error | | Name | Args | Returns |
| [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!) | `:trace` | `form` | `opts`/`id` | Form result | | :---------------------------------------------------------------------------------------------------------- | :------------------------- | :--------------------------- |
| [`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) | `:spy` | `form` | `opts`/`level` | Form result | | [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) | `[opts]` or `[?level msg]` | nil |
| [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) | `:error` | `form` | `opts`/`id` | Form value or given fallback | | [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) | `[opts]` or `[id ?level]` | nil |
| [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) | `<arb>` | `opts` | - | Depends on opts | | [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!) | `[opts]` or `[?id run]` | Form result |
| [`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) | `[opts]` or `[?level run]` | Form result |
| [`error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error!) | `[opts]` or `[?id error]` | Given error |
| [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) | `[opts]` or `[?id error]` | Form value or given fallback |
| [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) | `[opts]` | Depends on opts |
### Internal help ### Internal help
Detailed help is available without leaving your IDE: Detailed help is available without leaving your IDE:
| Var | Help with | | Var | Help with |
| :---------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | Creating signals | | [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | Creating signals |
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options when creating signals | | [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options when creating signals |
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal content (map given to middleware/handlers) | | [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal content (map given to transforms/handlers) |
| [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation | | [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation |
| [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management | | [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management |
| [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options | | [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options |
| [`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. | | [`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 |
### Included handlers ## Performance
See linked docstrings below for features and usage: Telemere is **highly optimized** and offers great performance at any scale, handling up to **4.2 million filtered signals/sec** on a 2020 Macbook Pro M1.
| Name | Platform | Output target | Output format | Signal call benchmarks (per thread):
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals for [cljs-devtools](https://github.com/binaryage/cljs-devtools), etc. |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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](https://github.com/open-telemetry/opentelemetry-java) exporters | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) and tracing data |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:slack`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) | Clj | [Slack](https://slack.com/) (via [clj-slack](https://github.com/julienXX/clj-slack)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
See [here](../../wiki/4-Handlers) for more/upcoming handlers, community handlers, info on **writing your own handlers**, etc. | Compile-time filtering? | Runtime filtering? | Profile? | Trace? | nsecs / call |
| :---------------------: | :----------------: | :------: | :----: | -----------: |
## Documentation
- [Wiki][GitHub wiki] (getting started, usage, etc.)
- API reference via [cljdoc][cljdoc docs] or [Codox][Codox docs]
- Extensive [internal help](#internal-help) (no need to leave your IDE)
- Support via [#Telemere Slack channel][] or [GitHub issues][]
- [General observability tips](../../wiki/7-Tips) (advice on building and maintaining observable Clojure/Script systems, and getting the most out of Telemere)
## Community
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, middleware, and utils.
See [here](../../wiki/8-Community) for community resources.
## Benchmarks
Telemere is **highly optimized** and offers great performance at any scale:
| Compile-time filtering? | Runtime filtering? | Profile? | Trace? | nsecs |
| :---------------------: | :----------------: | :------: | :----: | ----: |
| ✓ (elide) | - | - | - | 0 | | ✓ (elide) | - | - | - | 0 |
| - | ✓ | - | - | 350 | | - | ✓ | - | - | 350 |
| - | ✓ | ✓ | - | 450 | | - | ✓ | ✓ | - | 450 |
| - | ✓ | ✓ | ✓ | 1000 | | - | ✓ | ✓ | ✓ | 1000 |
Measurements: - Nanoseconds per signal call ~ **milliseconds per 1e6 calls**
- Times exclude [handler runtime](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) (which depends on handler/s, is usually async)
- Are **~nanoseconds per signal call** (= milliseconds per 1e6 calls) - Benched on a 2020 Macbook Pro M1, running Clojure v1.12 and OpenJDK v22
- Exclude [handler runtime](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) (which depends on handler/s, is usually async)
- Taken on a 2020 Macbook Pro M1, running Clojure v1.12 and OpenJDK v22
### Performance philosophy ### Performance philosophy
Telemere is optimized for *real-world* performance. This means **prioritizing flexibility** and realistic usage over synthetic micro benchmarks. Telemere is optimized for *real-world* performance. This means **prioritizing flexibility** and realistic usage over synthetic micro-benchmarks.
Large applications can produce absolute *heaps* of data, not all equally valuable. Quickly processing infinite streams of unmanageable junk is an anti-pattern. As scale and complexity increase, it becomes more important to **strategically plan** what data to collect, when, in what quantities, and how to manage it. Large applications can produce absolute *heaps* of data, not all equally valuable. Quickly processing infinite streams of unmanageable junk is an anti-pattern. As scale and complexity increase, it becomes more important to **strategically plan** what data to collect, when, in what quantities, and how to manage it.
Telemere is designed to help with all that. It offers [rich data](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) and unmatched [filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) support - including per-signal and per-handler **sampling** and **rate-limiting**. Telemere is designed to help with all that. It offers [rich data](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) and unmatched [filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) support - including per-signal and per-handler **sampling** and **rate limiting**, and zero cost compile-time filtering.
Use these to ensure that you're not capturing useless/low-value/high-noise information in production! With appropriate planning, Telemere is designed to scale to systems of any size and complexity. Use these to ensure that you're not capturing useless/low-value/high-noise information in production! With appropriate planning, Telemere is designed to scale to systems of any size and complexity.
See [here](../../wiki/7-Tips) for detailed tips on real-world usage. See [here](../../wiki/7-Tips) for detailed tips on real-world usage.
## Included handlers
See ✅ links below for **features and usage**,
See ❤️ links below to **vote on future handlers**:
| Target (↓) | Clj | Cljs |
| :--------------------------------------------- | :-----------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------: |
| [Apache Kafka](https://kafka.apache.org/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [AWS Kinesis](https://aws.amazon.com/kinesis/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| 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) |
| Console (raw) | - | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) |
| [Datadog](https://www.datadoghq.com/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | [❤️](https://github.com/taoensso/roadmap/issues/12) |
| Email | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | - |
| [Graylog](https://graylog.org/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [Jaeger](https://www.jaegertracing.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [Logstash](https://www.elastic.co/logstash) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [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) |
| [Redis](https://redis.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| SQL | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [Slack](https://slack.com/) | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) | - |
| TCP socket | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | - |
| UDP socket | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | - |
| [Zipkin](https://zipkin.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
You can also easily [write your own handlers](../../wiki/4-Handlers#writing-handlers).
## Community
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.
See [here](../../wiki/8-Community) for community resources.
## Documentation
- [Wiki][GitHub wiki] (getting started, usage, etc.)
- API reference via [cljdoc][cljdoc]
- Extensive [internal help](#internal-help) (no need to leave your IDE)
- Support via [Slack][] or [GitHub issues][]
- [General observability tips](../../wiki/7-Tips) (advice on building and maintaining observable Clojure/Script systems, and getting the most out of Telemere)
## Funding ## Funding
You can [help support][sponsor] continued work on this project, thank you!! 🙏 You can [help support][sponsor] continued work on this project and [others][my work], thank you!! 🙏
## License ## License
Copyright &copy; 2023-2024 [Peter Taoussanis][]. Copyright &copy; 2023-2025 [Peter Taoussanis][].
Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure). Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure).
<!-- Common --> <!-- Common -->
@ -250,20 +331,22 @@ Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure).
[GitHub releases]: ../../releases [GitHub releases]: ../../releases
[GitHub issues]: ../../issues [GitHub issues]: ../../issues
[GitHub wiki]: ../../wiki [GitHub wiki]: ../../wiki
[Slack channel]: https://www.taoensso.com/telemere/slack [Slack]: https://www.taoensso.com/telemere/slack
[Peter Taoussanis]: https://www.taoensso.com [Peter Taoussanis]: https://www.taoensso.com
[sponsor]: https://www.taoensso.com/sponsor [sponsor]: https://www.taoensso.com/sponsor
[my work]: https://www.taoensso.com/clojure-libraries
<!-- Project --> <!-- Project -->
[Codox docs]: https://taoensso.github.io/telemere/ [cljdoc]: https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere
[cljdoc docs]: https://cljdoc.org/d/com.taoensso/telemere/
[Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/telemere.svg [Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/telemere.svg
[Clojars URL]: https://clojars.org/com.taoensso/telemere [Clojars URL]: https://clojars.org/com.taoensso/telemere
[Main tests SVG]: https://github.com/taoensso/telemere/actions/workflows/main-tests.yml/badge.svg [Clj tests SVG]: https://github.com/taoensso/telemere/actions/workflows/clj-tests.yml/badge.svg
[Main tests URL]: https://github.com/taoensso/telemere/actions/workflows/main-tests.yml [Clj tests URL]: https://github.com/taoensso/telemere/actions/workflows/clj-tests.yml
[Cljs tests SVG]: https://github.com/taoensso/telemere/actions/workflows/cljs-tests.yml/badge.svg
[Cljs tests URL]: https://github.com/taoensso/telemere/actions/workflows/cljs-tests.yml
[Graal tests SVG]: https://github.com/taoensso/telemere/actions/workflows/graal-tests.yml/badge.svg [Graal tests SVG]: https://github.com/taoensso/telemere/actions/workflows/graal-tests.yml/badge.svg
[Graal tests URL]: https://github.com/taoensso/telemere/actions/workflows/graal-tests.yml [Graal tests URL]: https://github.com/taoensso/telemere/actions/workflows/graal-tests.yml

View file

@ -1,213 +1,256 @@
(ns examples (ns examples
"Basic Telemere usage examples that appear in the Wiki or docstrings." "Basic Telemere usage examples that appear in the Wiki or docstrings."
(:require [taoensso.telemere :as t])) (:require [taoensso.telemere :as tel]))
;;;; README "Quick example" (comment
(require '[taoensso.telemere :as t]) ;;;; README "Quick examples"
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message") ; %> (require '[taoensso.telemere :as tel])
;; 2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message
;; data: {:x1 :x2}
(t/log! "This will send a `:log` signal to the Clj/s console") ;; No config needed for typical use cases!!
(t/log! :info "This will do the same, but only when the current level is >= `:info`") ;; Signals print to console by default for both Clj and Cljs
;; Easily construct messages from parts ;; Traditional style logging (data formatted into message string):
(t/log! :info ["Here's a" "joined" "message!"]) (tel/log! {:level :info, :msg (str "User " 1234 " logged in!")})
;; Attach an id ;; Modern/structured style logging (explicit id and data)
(t/log! {:level :info, :id ::my-id} "This signal has an id") (tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}})
;; Attach arb data ;; Mixed style (explicit id and data, with message string)
(t/log! {:level :info, :data {:x :y}} "This signal has structured data") (tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}, :msg "User logged in!"})
;; Capture for debug/testing ;; Trace (can interop with OpenTelemetry)
(t/with-signal (t/log! "This will be captured")) ;; Tracks form runtime, return value, and (nested) parent tree
;; => {:keys [location level id data msg_ ...]} (tel/trace! {:id ::my-id :data {...}}
(do-some-work))
;; `:let` bindings are available to `:data` and message, but only paid ;; Check resulting signal content for debug/tests
;; for when allowed by minimum level and other filtering criteria (tel/with-signal (tel/log! {...})) ; => {:keys [ns level id data msg_ ...]}
(t/log!
{:level :info
:let [expensive-metric1 (last (for [x (range 100), y (range 100)] (* x y)))]
:data {:metric1 expensive-metric1}}
["Message with metric value:" expensive-metric1])
;; With sampling 50% and 1/sec rate limiting ;; Getting fancy (all costs are conditional!)
(t/log! (tel/log!
{:sample-rate 0.5 {:level :debug
:rate-limit {"1 per sec" [1 1000]}} :sample 0.75 ; 75% sampling (noop 25% of the time)
"This signal will be sampled and rate limited") :when (my-conditional)
:limit {"1 per sec" [1 1000]
"5 per min" [5 60000]} ; Rate limit
:limit-by my-user-ip-address ; Rate limit scope
;; There are several signal creators available for convenience. :do (inc-my-metric!)
;; All support the same options but each offer's a calling API :let
;; optimized for a particular use case. Compare: [diagnostics (my-expensive-diagnostics)
formatted (my-expensive-format diagnostics)]
;; `log!` - [msg] or [level-or-opts msg] :data
(t/with-signal (t/log! {:level :info, :id ::my-id} "Hi!")) {:diagnostics diagnostics
:formatted formatted
:local-state *my-dynamic-context*}}
;; `event!` - [id] or [id level-or-opts] ;; Message string or vector to join as string
(t/with-signal (t/event! ::my-id {:level :info, :msg "Hi!"})) ["Something interesting happened!" formatted])
)
;; `signal!` - [opts] ;; Set minimum level
(t/with-signal (t/signal! {:level :info, :id ::my-id, :msg "Hi!"})) (tel/set-min-level! :warn) ; For all signals
(tel/set-min-level! :log :debug) ; For `log!` signals specifically
;; See `t/help:signal-creators` docstring for more ;; Set id and namespace filters
(tel/set-id-filter! {:allow #{::my-particular-id "my-app/*"}})
(tel/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"})
;;; A quick taste of filtering ;; SLF4J signals will have their `:ns` key set to the logger's name
;; (typically a source class)
(tel/set-ns-filter! {:disallow "com.noisy.java.package.*"})
(t/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"}) ; Set namespace filter ;; Set minimum level for `log!` signals for particular ns pattern
(t/set-id-filter! {:allow #{::my-particular-id "my-app/*"}}) ; Set id filter (tel/set-min-level! :log "taoensso.sente.*" :warn)
(t/set-min-level! :warn) ; Set minimum level for all signals ;; Use transforms (xfns) to filter and/or arbitrarily modify signals
(t/set-min-level! :log :debug) ; Set minimul level for `log!` signals ;; by signal data/content/etc.
;; Set minimum level for `event!` signals originating in (tel/set-xfn!
;; the "taoensso.sente.*" ns
(t/set-min-level! :event "taoensso.sente.*" :warn)
;; See `t/help:filters` docstring for more
;;; Use middleware to transform signals and/or filter signals
;;; by signal data/content/etc.
(t/set-middleware!
(fn [signal] (fn [signal]
(if (get-in signal [:data :hide-me?]) (if (-> signal :data :skip-me?)
nil ; Suppress signal (don't handle) nil ; Filter signal (don't handle)
(assoc signal :passed-through-middleware? true)))) (assoc signal :transformed? true))))
(t/with-signal (t/event! ::my-id {:data {:hide-me? true}})) ; => nil (tel/with-signal (tel/log! {... :data {:skip-me? true}})) ; => nil
(t/with-signal (t/event! ::my-id {:data {:hide-me? false}})) ; => {...} (tel/with-signal (tel/log! {... :data {:skip-me? false}})) ; => {...}
;;;; Docstring snippets ;; See `tel/help:filters` docstring for more filtering options
(t/with-signal (t/event! ::my-id)) ;; Add your own signal handler
(t/with-signal (t/event! ::my-id :warn)) (tel/add-handler! :my-handler
(t/with-signal (fn
(t/event! ::my-id ([signal] (println signal))
([] (println "Handler has shut down"))))
;; Use `add-handler!` to set handler-level filtering and back-pressure
(tel/add-handler! :my-handler
(fn
([signal] (println signal))
([] (println "Handler has shut down")))
{:async {:mode :dropping, :buffer-size 1024, :n-threads 1}
:priority 100
:sample 0.5
:min-level :info
:ns-filter {:disallow "taoensso.*"}
:limit {"1 per sec" [1 1000]}
;; See `tel/help:handler-dispatch-options` for more
})
;; See current handlers
(tel/get-handlers) ; => {<handler-id> {:keys [handler-fn handler-stats_ dispatch-opts]}}
;; Add console handler to print signals as human-readable text
(tel/add-handler! :my-handler
(tel/handler:console
{:output-fn (tel/format-signal-fn {})}))
;; Add console handler to print signals as edn
(tel/add-handler! :my-handler
(tel/handler:console
{:output-fn (tel/pr-signal-fn {:pr-fn :edn})}))
;; Add console handler to print signals as JSON
;; Ref. <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista]))
(tel/add-handler! :my-handler
(tel/handler:console
{:output-fn
#?(:cljs :json ; Use js/JSON.stringify
:clj jsonista/write-value-as-string)}))
;;;; Docstring examples
(tel/with-signal (tel/event! ::my-id))
(tel/with-signal (tel/event! ::my-id :warn))
(tel/with-signal
(tel/event! ::my-id
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x} :data {:x x}
:msg ["My msg:" x]})) :msg ["My msg:" x]}))
(t/with-signal (t/log! "My msg")) (tel/with-signal (tel/log! "My msg"))
(t/with-signal (t/log! :warn "My msg")) (tel/with-signal (tel/log! :warn "My msg"))
(t/with-signal (tel/with-signal
(t/log! (tel/log!
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}} :data {:x x}}
["My msg:" x])) ["My msg:" x]))
(t/with-signal (throw (t/error! (ex-info "MyEx" {})))) (tel/with-signal (throw (tel/error! (ex-info "MyEx" {}))))
(t/with-signal (throw (t/error! ::my-id (ex-info "MyEx" {})))) (tel/with-signal (throw (tel/error! ::my-id (ex-info "MyEx" {}))))
(t/with-signal (tel/with-signal
(throw (throw
(t/error! (tel/error!
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x} :data {:x x}
:msg ["My msg:" x]} :msg ["My msg:" x]}
(ex-info "MyEx" {})))) (ex-info "MyEx" {}))))
(t/with-signal (t/trace! (+ 1 2))) (tel/with-signal (tel/trace! (+ 1 2)))
(t/with-signal (t/trace! ::my-id (+ 1 2))) (tel/with-signal (tel/trace! ::my-id (+ 1 2)))
(t/with-signal (tel/with-signal
(t/trace! (tel/trace!
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}} :data {:x x}}
(+ 1 2))) (+ 1 2)))
(t/with-signal (t/spy! (+ 1 2))) (tel/with-signal (tel/spy! (+ 1 2)))
(t/with-signal (t/spy! :debug (+ 1 2))) (tel/with-signal (tel/spy! :debug (+ 1 2)))
(t/with-signal (tel/with-signal
(t/spy! (tel/spy!
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}} :data {:x x}}
(+ 1 2))) (+ 1 2)))
(t/with-signal (t/catch->error! (/ 1 0))) (tel/with-signal (tel/catch->error! (/ 1 0)))
(t/with-signal (t/catch->error! ::my-id (/ 1 0))) (tel/with-signal (tel/catch->error! ::my-id (/ 1 0)))
(t/with-signal (tel/with-signal
(t/catch->error! (tel/catch->error!
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x} :data {:x x}
:msg ["My msg:" x my-error] :msg ["My msg:" x]
:catch-val "Return value when form throws" :catch-val "Return value when form throws"}
:catch-sym my-error}
(/ 1 0))) (/ 1 0)))
;;;; Wiki examples ;;;; Wiki examples
;;; Filter signals ;;; Filter signals
(t/set-min-level! :info) ; Set global minimum level (tel/set-min-level! :info) ; Set global minimum level
(t/with-signal (t/event! ::my-id1 :info)) ; => {:keys [inst id ...]} (tel/with-signal (tel/event! ::my-id1 :info)) ; => {:keys [inst id ...]}
(t/with-signal (t/event! ::my-id1 :debug)) ; => nil (signal not allowed) (tel/with-signal (tel/event! ::my-id1 :debug)) ; => nil (signal not allowed)
(t/with-min-level :trace ; Override global minimum level (tel/with-min-level :trace ; Override global minimum level
(t/with-signal (t/event! ::my-id1 :debug))) ; => {:keys [inst id ...]} (tel/with-signal (tel/event! ::my-id1 :debug))) ; => {:keys [inst id ...]}
;; Disallow all signals in matching namespaces ;; Disallow all signals in matching namespaces
(t/set-ns-filter! {:disallow "some.nosy.namespace.*"}) (tel/set-ns-filter! {:disallow "some.nosy.namespace.*"})
;;; Configuring handlers ;;; Configuring handlers
;; Create a test signal ;; Create a test signal
(def my-signal (def my-signal
(t/with-signal (tel/with-signal
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message"))) (tel/log! {:id ::my-id, :data {:x1 :x2}} "My message")))
;; Create console handler with default opts (writes formatted string) ;; Create console handler with default opts (writes formatted string)
(def my-handler (t/handler:console)) (def my-handler (tel/handler:console {}))
;; Test handler, remember it's just a (fn [signal]) ;; Test handler, remember it's just a (fn [signal])
(my-handler my-signal) ; %> (my-handler my-signal) ; %>
;; 2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message ;; 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My message
;; data: {:x1 :x2} ;; data: {:x1 :x2}
;; Create console handler which writes signals as edn ;; Create console handler which writes signals as edn
(def my-handler (def my-handler
(t/handler:console (tel/handler:console
{:output-fn (t/pr-signal-fn {:pr-fn :edn})})) {:output-fn (tel/pr-signal-fn {:pr-fn :edn})}))
(my-handler my-signal) ; %> (my-handler my-signal) ; %>
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...} ;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}
;; Create console handler which writes signals as JSON ;; Create console handler which writes signals as JSON
;; Ref. <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista])) #?(:clj (require '[jsonista.core :as jsonista]))
(def my-handler (def my-handler
(t/handler:console (tel/handler:console
{:output-fn {:output-fn
(t/pr-signal-fn (tel/pr-signal-fn
{:pr-fn {:pr-fn
#?(:cljs :json #?(:cljs :json ; Use js/JSON.stringify
:clj jsonista.core/write-value-as-string)})})) :clj jsonista/write-value-as-string)})}))
(my-handler my-signal) ; %> (my-handler my-signal) ; %>
;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...} ;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...}
;; Deregister the default console handler ;; Deregister the default console handler
(t/remove-handler! :default/console) (tel/remove-handler! :defaultel/console)
;; Register our custom console handler ;; Register our custom console handler
(t/add-handler! :my-handler my-handler (tel/add-handler! :my-handler my-handler
;; Lots of options here for filtering, etc. ;; Lots of options here for filtering, etc.
;; See `help:handler-dispatch-options` docstring! ;; See `help:handler-dispatch-options` docstring!
{}) {})
;; NB make sure to always stop handlers at the end of your ;; NB make sure to always stop handlers at the end of your
;; `-main` or shutdown procedure ;; `-main` or shutdown procedure
(t/call-on-shutdown! t/stop-handlers!) (tel/call-on-shutdown!
(fn [] (tel/stop-handlers!)))
;; See `t/help:handlers` docstring for more ;; See `tel/help:handlers` docstring for more
;;; Writing handlers ;;; Writing handlers
;; Handlers are just fns of 2 arities ;; Handlers are just fns of 2 arities
(defn my-basic-handler (defn my-basic-handler
([signal] (println signal)) ; Arity-1 called when handling a signal
([]) ; Arity-0 called when stopping the handler ([]) ; Arity-0 called when stopping the handler
([signal] (println signal)) ; Arity-1 called when handling a signal
) )
;; If you're making a customizable handler for use by others, it's often ;; If you're making a customizable handler for use by others, it's often
@ -234,7 +277,7 @@
;; Do option validation and other prep here, i.e. try to keep ;; Do option validation and other prep here, i.e. try to keep
;; expensive work outside handler function when possible! ;; expensive work outside handler function when possible!
(let [handler-fn ; Fn of exactly 2 arities (let [handler-fn ; Fn of exactly 2 arities (1 and 0)
(fn a-handler:my-fancy-handler ; Note fn naming convention (fn a-handler:my-fancy-handler ; Note fn naming convention
([signal] ; Arity-1 called when handling a signal ([signal] ; Arity-1 called when handling a signal
@ -253,7 +296,7 @@
(with-meta handler-fn (with-meta handler-fn
{:dispatch-opts {:dispatch-opts
{:min-level :info {:min-level :info
:rate-limit :limit
[[1 1000] ; Max 1 signal per second [[1 1000] ; Max 1 signal per second
[10 60000] ; Max 10 signals per minute [10 60000] ; Max 10 signals per minute
]}})))) ]}}))))
@ -261,18 +304,18 @@
;;; Message building ;;; Message building
;; A fixed message (string arg) ;; A fixed message (string arg)
(t/log! "A fixed message") ; %> {:msg "A fixed message"} (tel/log! "A fixed message") ; %> {:msg "A fixed message"}
;; A joined message (vector arg) ;; A joined message (vector arg)
(let [user-arg "Bob"] (let [user-arg "Bob"]
(t/log! ["User" (str "`" user-arg "`") "just logged in!"])) (tel/log! ["User" (str "`" user-arg "`") "just logged in!"]))
;; %> {:msg_ "User `Bob` just logged in!` ...} ;; %> {:msg_ "User `Bob` just logged in!` ...}
;; With arg prep ;; With arg prep
(let [user-arg "Bob" (let [user-arg "Bob"
usd-balance-str "22.4821"] usd-balance-str "22.4821"]
(t/log! (tel/log!
{:let {:let
[username (clojure.string/upper-case user-arg) [username (clojure.string/upper-case user-arg)
usd-balance (parse-double usd-balance-str)] usd-balance (parse-double usd-balance-str)]
@ -285,24 +328,54 @@
;; %> {:msg "User BOB has balance: $22" ...} ;; %> {:msg "User BOB has balance: $22" ...}
(t/log! (str "This message " "was built " "by `str`")) (tel/log! (str "This message " "was built " "by `str`"))
;; %> {:msg "This message was built by `str`"} ;; %> {:msg "This message was built by `str`"}
(t/log! (format "This message was built by `%s`" "format")) (tel/log! (enc/format "This message was built by `%s`" "format"))
;; %> {:msg "This message was built by `format`"} ;; %> {:msg "This message was built by `format`"}
;;; App-level kvs ;;; App-level kvs
(t/with-signal (tel/with-signal
(t/event! ::my-id (tel/event! ::my-id
{:my-middleware-data "foo" {:my-data-for-xfn "foo"
:my-handler-data "bar"})) :my-data-for-handler "bar"}))
;; %> ;; %>
;; {;; App-level kvs included inline (assoc'd to signal root) ;; {;; App-level kvs included inline (assoc'd to signal root)
;; :my-middleware-data "foo" ;; :my-data-for-xfn "foo"
;; :my-handler-data "bar" ;; :my-data-for-handler "bar"
;; :kvs ; And also collected together under ":kvs" key ;; :kvs ; And also collected together under ":kvs" key
;; {:my-middleware-data "foo" ;; {:my-data-for-xfn "foo"
;; :my-handler-data "bar"} ;; :my-data-for-handler "bar"}
;; ... } ;; ... }
;;;; Misc extra examples
(tel/log! {:id ::my-id, :data {:x1 :x2}} ["My 2-part" "message"]) ; %>
;; 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My 2-part message
;; data: {:x1 :x2}
;; `:let` bindings are available to `:data` and message, but only paid
;; for when allowed by minimum level and other filtering criteria
(tel/log!
{:level :info
:let [expensive (reduce * (range 1 12))] ; 12 factorial
:data {:my-metric expensive}}
["Message with metric:" expensive])
;; With sampling 50% and 1/sec rate limiting
(tel/log!
{:sample 0.5
:limit {"1 per sec" [1 1000]}}
"This signal will be sampled and rate limited")
;; Several signal creators are available for convenience.
;; All offer the same options, but each has an API optimized
;; for a particular use case:
(tel/log! {:level :info, :id ::my-id} "Hi!") ; [msg] or [level-or-opts msg]
(tel/event! ::my-id {:level :info, :msg "Hi!"}) ; [id] or [id level-or-opts]
(tel/signal! {:level :info, :id ::my-id, :msg "Hi!"}) ; [opts]
)

View file

@ -1 +0,0 @@
../src/taoensso/telemere/carmine.clj

View file

@ -1 +1 @@
../src/taoensso/telemere/consoles.cljc ../main/src/taoensso/telemere/consoles.cljc

View file

@ -1 +1 @@
../src/taoensso/telemere/files.clj ../main/src/taoensso/telemere/files.clj

View file

@ -1 +0,0 @@
../src/taoensso/telemere/logstash.clj

View file

@ -1 +1 @@
../src/taoensso/telemere/open_telemetry.clj ../main/src/taoensso/telemere/open_telemetry.clj

View file

@ -1 +1 @@
../src/taoensso/telemere/postal.clj ../main/src/taoensso/telemere/postal.clj

View file

@ -1 +1 @@
../src/taoensso/telemere/slack.clj ../main/src/taoensso/telemere/slack.clj

View file

@ -1 +1 @@
../src/taoensso/telemere/sockets.clj ../main/src/taoensso/telemere/sockets.clj

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

5
install.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cd main; lein install; cd ..;
cd slf4j; lein install; cd ..;

View file

View file

8
main/jaeger.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
docker run --rm \
-p 16686:16686 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
open "http://localhost:16686"

View file

@ -1,22 +1,24 @@
(defproject com.taoensso/telemere "1.0.0-beta18" (defproject com.taoensso/telemere "1.2.1"
:author "Peter Taoussanis <https://www.taoensso.com>" :author "Peter Taoussanis <https://www.taoensso.com>"
:description "Structured telemetry library for Clojure/Script" :description "Structured logs and telemetry for Clojure/Script"
:url "https://www.taoensso.com/telemere" :url "https://www.taoensso.com/telemere"
:license :license
{:name "Eclipse Public License - v 1.0" {:name "Eclipse Public License - v 1.0"
:url "https://www.eclipse.org/legal/epl-v10.html"} :url "https://www.eclipse.org/legal/epl-v10.html"}
:scm {:name "git" :url "https://github.com/taoensso/telemere"}
:dependencies :dependencies
[[com.taoensso/encore "3.115.0"]] [[com.taoensso/encore "3.159.0"]]
:test-paths ["test" #_"src"] :test-paths ["test" #_"src"]
:profiles :profiles
{;; :default [:base :system :user :provided :dev] {;; :default [:base :system :user :provided :dev]
:provided {:dependencies [[org.clojure/clojurescript "1.11.132"] :provided {:dependencies [[org.clojure/clojurescript "1.12.134"]
[org.clojure/clojure "1.11.4"]]} [org.clojure/clojure "1.11.4"]]}
:c1.12 {:dependencies [[org.clojure/clojure "1.12.0-rc1"]]} :c1.12 {:dependencies [[org.clojure/clojure "1.12.3"]]}
:c1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]} :c1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]}
:c1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]} :c1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]}
@ -30,6 +32,8 @@
[com.github.clj-easy/graal-build-time "1.0.5"]]} [com.github.clj-easy/graal-build-time "1.0.5"]]}
:test {:aot [] #_[taoensso.telemere-tests]} :test {:aot [] #_[taoensso.telemere-tests]}
:ott-on {:jvm-opts ["-Dtaoensso.telemere.otel-tracing=true"]}
:ott-off {:jvm-opts ["-Dtaoensso.telemere.otel-tracing=false"]}
:dev :dev
{:jvm-opts {:jvm-opts
["-server" ["-server"
@ -42,31 +46,28 @@
*unchecked-math* false #_:warn-on-boxed} *unchecked-math* false #_:warn-on-boxed}
:dependencies :dependencies
[[org.clojure/test.check "1.1.1"] [[org.clojure/core.async "1.8.741"]
[org.clojure/test.check "1.1.2"]
[org.clojure/tools.logging "1.3.0"] [org.clojure/tools.logging "1.3.0"]
[org.slf4j/slf4j-api "2.0.14"] [org.slf4j/slf4j-api "2.0.17"]
[com.taoensso/slf4j-telemere "1.0.0-beta18"] [com.taoensso/telemere-slf4j "1.2.1"]
#_[org.slf4j/slf4j-simple "2.0.14"] #_[org.slf4j/slf4j-simple "2.0.16"]
#_[org.slf4j/slf4j-nop "2.0.14"] #_[org.slf4j/slf4j-nop "2.0.16"]
#_[io.github.paintparty/bling "0.4.2"]
;;; For optional handlers ;;; For optional handlers
[io.opentelemetry/opentelemetry-api "1.41.0"] [io.opentelemetry/opentelemetry-api "1.57.0"]
#_[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.41.0"] [io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.57.0"]
#_[io.opentelemetry/opentelemetry-exporter-otlp "1.41.0"] [io.opentelemetry/opentelemetry-exporter-otlp "1.57.0"]
#_[io.opentelemetry/opentelemetry-exporters-jaeger "0.9.1"] #_[io.opentelemetry/opentelemetry-exporters-jaeger "0.9.1"]
[metosin/jsonista "0.3.10"] [metosin/jsonista "0.3.13"]
[com.draines/postal "2.0.5"] [com.draines/postal "2.0.5"]
[org.julienxx/clj-slack "0.8.3"]] [org.julienxx/clj-slack "0.8.3"]]
:plugins :plugins
[[lein-pprint "1.3.2"] [[lein-pprint "1.3.2"]
[lein-ancient "0.7.0"] [lein-ancient "0.7.0"]
[lein-cljsbuild "1.1.8"] [lein-cljsbuild "1.1.8"]]}}
[com.taoensso.forks/lein-codox "0.10.11"]]
:codox
{:language #{:clojure :clojurescript}
:base-language :clojure}}}
:cljsbuild :cljsbuild
{:test-commands {"node" ["node" "target/test.js"]} {:test-commands {"node" ["node" "target/test.js"]}
@ -91,4 +92,6 @@
"test-clj" ["with-profile" "+c1.12:+c1.11:+c1.10" "test"] "test-clj" ["with-profile" "+c1.12:+c1.11:+c1.10" "test"]
"test-cljs" ["with-profile" "+c1.12" "cljsbuild" "test"] "test-cljs" ["with-profile" "+c1.12" "cljsbuild" "test"]
"test-all" ["do" ["clean"] ["test-clj"] ["test-cljs"]]})
"test-clj-ott-off" ["with-profile" "+ott-off" "test-clj"]
"test-all" ["do" ["clean"] ["test-clj"] ["test-clj-ott-off"] ["test-cljs"]]})

View file

@ -1,12 +1,15 @@
Unconditionally executes given form and- ALWAYS (unconditionally) executes given `run` form and:
If form succeeds: return the form's result.
If form throws:
Call `error!` with the thrown error and the given signal options [2],
then return (:catch-val opts) if it exists, or rethrow the error.
API: [form] [id-or-opts form] => form's result (value/throw) (unconditional), or (:catch-val opts)
Default kind: `:error` Default kind: `:error`
Default level: `:error` Default level: `:error`
Returns:
- If given `run` form succeeds: returns the form's result.
- If given `run` form throws ANYTHING:
Calls `error!` with the thrown error and given signal options [2], then
either returns given (:catch-val opts), or rethrows.
Just a convenience util. For more flexibility use your own `try/catch`.
See `taoensso.encore/try*` for easily catching cross-platform errors.
Examples: Examples:
@ -15,19 +18,18 @@ Examples:
(catch->error! (catch->error!
{:let [x "x"] ; Available to `:data` and `:msg` {:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x} :data {:x x}
:msg ["My msg:" x my-error] :msg ["My msg:" x]
:catch-val "Return value when form throws" :catch-val "Return value iff form throws"}
:catch-sym my-error ; Sym of caught error, available to `:data` and `:msg`
}
(/ 1 0)) ; %> {... :data {x "x"}, :msg_ "My msg: x <caught>" ...} (/ 1 0)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
Tips: Tips:
- Test using `with-signal`: (with-signal (catch->error! ...)). - Test using `with-signal`: (with-signal (catch->error! ...)).
- Supports the same options [2] as other signals [1]. - Supports the same options [2] as other signals [1].
- Useful for recording errors in forms, futures, callbacks, etc. - Useful for preventing errors from going unnoticed in futures, callbacks,
agent actions, etc.!: (future (catch->error ::my-future (do-something)))
See also `error!`. See also `error!`.

View file

@ -0,0 +1,60 @@
Telemere supports extensive environmental config via JVM properties,
environment variables, or classpath resources.
Environmental filter config includes:
1. Minimum level (see signal `:level`):
a. JVM property: `taoensso.telemere.rt-min-level`
b. Env variable: `TAOENSSO_TELEMERE_RT_MIN_LEVEL`
c. Classpath resource: `taoensso.telemere.rt-min-level`
2. Namespace filter (see signal `:ns`):
a. JVM property: `taoensso.telemere.rt-ns-filter`
b. Env variable: `TAOENSSO_TELEMERE_RT_NS_FILTER`
c. Classpath resource: `taoensso.telemere.rt-ns-filter`
3. Id filter (see signal `:id`):
a. JVM property: `taoensso.telemere.rt-id-filter`
b. Env variable: `TAOENSSO_TELEMERE_RT_ID_FILTER`
c. Classpath resource: `taoensso.telemere.rt-id-filter`
4. Kind filter (signal `:kind`):
a. JVM property: `taoensso.telemere.rt-kind-filter`
b. Env variable: `TAOENSSO_TELEMERE_RT_KIND_FILTER`
c. Classpath resource: `taoensso.telemere.rt-kind-filter`
Config values are parsed as edn, examples:
`taoensso.telemere.rt-min-level` => ":info"
`TAOENSSO_TELEMERE_RT_NS_FILTER` => "{:disallow \"taoensso.*\"}"
`taoensso.telemere.rt-id-filter.cljs` => "#{:my-id1 :my-id2}"
`TAOENSSO_TELEMERE_RT_KIND_FILTER_CLJ` => "nil"
Runtime vs compile-time filters
The above filters (1..4) all apply at RUNTIME ("rt").
This is typically what you want, since it allows you to freely adjust filtering
(making it less OR MORE permissive) through later API calls like `set-min-level!`.
As an advanced option, you can instead/additionally ELIDE (entirely omit) filtered
callsites at COMPILE-TIME ("ct") by replacing "rt"->"ct" / "RT"->"CT" in the config
ids above. Compile-time filters CANNOT be made MORE permissive at runtime.
Tips:
- The above config ids will affect both Clj AND Cljs.
For platform-specific filters, use
".clj" / "_CLJ" or
".cljs" / "_CLJS" suffixes instead.
e.g. "taoensso.telemere.rt-min-level.cljs".
- To get the right edn syntax, first set your runtime filters using the
standard utils (`set-min-level!`, etc.). Then call `get-filters` and
serialize the relevant parts to edn with `pr-str`.
- All environmental config uses `get-env` underneath.
See the `get-env` docstring for more/advanced details.
- Classpath resources are files accessible on your project's
classpath. This usually includes files in your project's
`resources/` dir.

View file

@ -1,8 +1,10 @@
"Error" signal creator, emphasizing error + id. "Error" signal creator, emphasizing (optional id) + error (Exception, etc.).
API: [error] [id-or-opts error] => given error (unconditional)
Default kind: `:error` Default kind: `:error`
Default level: `:error` Default level: `:error`
Returns:
ALWAYS (unconditionally) returns the given error, so can conveniently be
wrapped by `throw`: (throw (error! (ex-info ...)), etc.
Examples: Examples:
@ -22,7 +24,6 @@ Tips:
- Supports the same options [2] as other signals [1]. - Supports the same options [2] as other signals [1].
- `error` arg is a platform error (`java.lang.Throwable` or `js/Error`). - `error` arg is a platform error (`java.lang.Throwable` or `js/Error`).
- Can conveniently be wrapped by `throw`: (throw (error! ...)).
---------------------------------------------------------------------- ----------------------------------------------------------------------
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...) [1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)

View file

@ -1,8 +1,10 @@
"Event" signal creator, emphasizing id + level. "Event" signal creator, emphasizing id + (optional level).
API: [id] [id level-or-opts] => true iff signal was allowed
Default kind: `:event` Default kind: `:event`
Default level: `:info` Default level: `:info`
Returns:
- For `event!` variant: nil, unconditionally.
- For `event!?` variant: true iff signal was created (allowed by filtering).
When filtering conditions are met [4], creates a Telemere signal [3] and When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to dispatches it to registered handlers for processing (e.g. writing to

View file

@ -1,8 +1,10 @@
"Log" signal creator, emphasizing message + level. "Log" signal creator, emphasizing (optional level) + message.
API: [msg] [level-or-opts msg] => true iff signal was allowed.
Default kind: `:log` Default kind: `:log`
Default level: `:info` Default level: `:info`
Returns:
- For `log!` variant: nil, unconditionally.
- For `log!?` variant: true iff signal was created (allowed by filtering).
When filtering conditions are met [4], creates a Telemere signal [3] and When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to dispatches it to registered handlers for processing (e.g. writing to

View file

@ -1,20 +1,22 @@
Low-level generic signal creator. Low-level "generic" signal creator for creating signals of any "kind".
Takes a single map of options [2] with compile-time keys.
API: [opts] => depends on options [2] Default kind: `:generic` (feel free to change!)
Default kind: none (optional) Default level: `:info`
Default level: none (must be provided) Returns:
- If given `:run` form: unconditionally returns run value, or rethrows run error.
- Otherwise: returns true iff signal was created (allowed by filtering).
When filtering conditions are met [4], creates a Telemere signal [3] and When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to dispatches it to registered handlers for processing (e.g. writing to
console/file/queue/db, etc.). console/file/queue/db, etc.).
If `:run` option is provided: returns value of given run form, or throws.
Otherwise: returns true iff signal was created (allowed).
Generic signals are fairly low-level and useful mostly for library authors or Generic signals are fairly low-level and useful mostly for library authors or
advanced users writing their own wrapper macros. Regular users will typically advanced users writing their own wrapper macros. NB see `keep-callsite` for
prefer one of the higher-level signal creators optimized for ease-of-use in preserving callsite coords when wrapping Telemere macros like `signal!`.
common cases [1].
Regular users will typically prefer one of the higher-level signal creators
optimized for ease-of-use in common cases [1].
Tips: Tips:

View file

@ -1,16 +1,19 @@
Signals are maps with {:keys [inst id ns level data msg_ ...]}, Telemere signals are maps with {:keys [inst id ns level data msg_ ...]},
though they can be modified by signal and/or handler middleware. though they can be modified by call and/or handler transform (xfns).
Default signal keys: Default signal keys:
`:schema` ------ Int version of signal schema (current: 1) `:schema` ------ Int version of signal schema (current: 1)
`:inst` -------- Platform instant [1] when signal was created `:inst` -------- Platform instant [1] when signal was created, monotonicity depends on system clock
`:level` ------- Signal level ∈ #{<int> :trace :debug :info :warn :error :fatal :report ...} `:ns` ---------- ?str namespace of signal callsite
`:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy :slf4j :tools-logging <app-val> ...} `:coords` ------ ?[line column] of signal callsite
`:id` ---------- ?id of signal (common to all signals created at callsite, contrast with `:uid`)
`:uid` --------- ?id of signal instance (unique to each signal created at callsite when tracing, contrast with `:id`)
`:msg` --------- Arb app-level message ?str given to signal creator `:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy :slf4j :tools-logging <app-val> ...}
`:level` ------- Signal level ∈ #{<int> :trace :debug :info :warn :error :fatal :report ...}
`:id` ---------- Signal callsite ?id (usu. keyword) (common to all signals created at callsite, contrast with `:uid`)
`:uid` --------- Signal instance ?id (usu. string) (unique to each signal created at callsite when tracing, contrast with `:id`)
`:msg_` -------- Arb app-level message ?str given to signal creator - may be a delay, always use `force` to unwrap!
`:data` -------- Arb app-level data ?val (usu. a map) given to signal creator `:data` -------- Arb app-level data ?val (usu. a map) given to signal creator
`:error` ------- Arb app-level platform ?error [2] given to signal creator `:error` ------- Arb app-level platform ?error [2] given to signal creator
@ -19,24 +22,18 @@ Default signal keys:
`:run-nsecs` --- ?int nanosecs runtime of `:run` ?form `:run-nsecs` --- ?int nanosecs runtime of `:run` ?form
`:end-inst` ---- Platform ?instant [1] when `:run` ?form completed `:end-inst` ---- Platform ?instant [1] when `:run` ?form completed
`:ctx` --------- ?val of `*ctx*` (arb app-level state) when signal was created
`:parent` ------ ?{:keys [id uid]} of parent signal, present in nested signals when tracing `:parent` ------ ?{:keys [id uid]} of parent signal, present in nested signals when tracing
`:root` -------- ?{:keys [id uid]} of root signal, present in nested signals when tracing `:root` -------- ?{:keys [id uid]} of root signal, present in nested signals when tracing
`:ctx` --------- ?val of `*ctx*` (arb app-level state) when signal was created
`:location` ---- ?{:keys [ns file line column]} signal creator callsite
`:ns` ---------- ?str namespace of signal creator callsite, same as (:ns location)
`: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)
`:host` -------- (Clj only) {:keys [name ip]} info for network host `:host` -------- (Clj only) {:keys [name ip]} info for network host
`:thread` ------ (Clj only) {:keys [name id group]} info for thread that created signal `:thread` ------ (Clj only) {:keys [name id group]} info for thread that created signal
`:sample-rate` - ?rate ∈ℝ[0,1] for combined signal AND handler sampling (0.75 => allow 75% of signals, nil => allow all) `:sample` ------ Sample ?rate ∈ℝ[0,1] for combined call AND handler sampling (0.75 => allow 75% of signals, nil => allow all)
<kvs> ---------- Other arb app-level ?kvs given to signal creator. Typically NOT included <kvs> ---------- Other arb app-level ?kvs given to signal creator. Typically NOT included
in handler output, so a great way to provide custom data/opts for use in handler output, so a great way to provide custom data/opts for use
(only) by custom middleware/handlers. (only) by custom transforms/handlers.
If anything is unclear, please ping me (@ptaoussanis) so that I can improve these docs! If anything is unclear, please ping me (@ptaoussanis) so that I can improve these docs!

View file

@ -11,16 +11,17 @@ various keys:
- All signal creators offer the same options [2], and - All signal creators offer the same options [2], and
- All signal kinds can contain the same content [3] - All signal kinds can contain the same content [3]
Creators vary only in in their default options and call APIs (expected args Creators vary only in in their default `:kind` value and call APIs (expected
and return values), making them more/less convenient for certain use cases: args and return values), making them more/less convenient for certain use cases:
`event!` -------- [id ] or [id opts/level] => true iff signal was created (allowed) `log!` ------------- ?level + msg => nil
`log!` ---------- [msg ] or [opts/level msg] => true iff signal was created (allowed) `event!` ----------- id + ?level => nil
`error!` -------- [error] or [opts/id error] => given error (unconditional) `trace!` ----------- ?id + run => run result (value or throw)
`trace!` -------- [form ] or [opts/id form] => form result (value/throw) (unconditional) `spy!` ------------- ?level + run => run result (value or throw)
`spy!` ---------- [form ] or [opts/level form] => form result (value/throw) (unconditional) `error!` ----------- ?id + error => given error
`catch->error!` - [form ] or [opts/id form] => form value, or given fallback `catch->error!` ---- ?id + run => run value or ?catch-val
`signal!` ------- [opts ] => depends on options `uncaught->error!` - ?id => nil
`signal!` ---------- opts => allowed? / run result (value or throw)
- `log!` and `event!` are both good default/general-purpose signal creators. - `log!` and `event!` are both good default/general-purpose signal creators.
- `log!` emphasizes messages, while `event!` emphasizes ids. - `log!` emphasizes messages, while `event!` emphasizes ids.

View file

@ -0,0 +1,49 @@
Signal options are provided as a map with COMPILE-TIME keys.
All options are available for all signal creator calls:
`:inst` -------- Platform instant [1] when signal was created, ∈ #{nil :auto <[1]>}
`:level` ------- Signal level ∈ #{<int> :trace :debug :info :warn :error :fatal :report ...}
`:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy <app-val> ...}
`:id` ---------- ?id of signal (common to all signals created at callsite, contrast with `:uid`)
`:uid` --------- ?id of signal instance (unique to each signal created at callsite, contrast with `:id`)
Defaults to `:auto` for tracing signals, and nil otherwise
`:msg` --------- Arb app-level ?message to incl. in signal: str or vec of strs to join (with `\space`), may be a delay
`:data` -------- Arb app-level ?data to incl. in signal: usu. a map, LAZY! [3]
`:error` ------- Arb app-level ?error to incl. in signal: platform error [2]
`:run` --------- ?form to execute UNCONDITIONALLY; will incl. `:run-val` in signal
`:do` ---------- ?form to execute conditionally (iff signal allowed) and LAZILY [3], before establishing `:let` ?binding
`:let` --------- ?bindings to establish conditionally (iff signal allowed) and LAZILY [3], BEFORE evaluating `:data` and `:msg` (useful!)
`:parent` ------ Custom ?{:keys [id uid]} to override auto (dynamic) parent signal tracing info
`:root` -------- Custom ?{:keys [id uid]} to override auto (dynamic) root signal tracing info
`:ctx` --------- Custom ?val to override auto (dynamic `*ctx*`) in signal, as per `with-ctx`
`:ctx+` -------- Custom ?val to update auto (dynamic `*ctx*`) in signal, as per `with-ctx+`
`:ns` ---------- Custom ?str namespace to override auto signal callsite info
`:coords` ------ Custom ?[line column] to override auto signal callsite info
`:elidable?` --- Should signal be subject to compile-time elision? (default true)
`:allow?` ------ Custom override for usual runtime filtering (true => ALWAYS create signal)
`:trace?` ------ Should tracing be enabled for `:run` form?
`:sample` ------ Sample ?rate ∈ℝ[0,1] for random signal sampling (0.75 => allow 75% of signals, nil => allow all)
`:when` -------- Arb ?form; when present, form must return truthy to allow signal
`:limit` ------- Rate limit ?spec given to `taoensso.telemere/rate-limiter`, see its docstring for details
`:limit-by` ---- When present, rate limits will be enforced independently for each value (any Clojure value!)
`:xfn` --------- Optional transform (fn [signal]) => ?modified-signal to apply when signal is created, as per `with-xfn`
`:xfn+` -------- Optional extra transform (fn [signal]) => ?modified-signal to apply when signal is created, as per `with-xfn+`
<kvs> ---------- Other arb app-level ?kvs to incl. in signal. Typically NOT included in
handler output, so a great way to provide custom data/opts for use
(only) by custom transforms/handlers. LAZY! [3]
If anything is unclear, please ping me (@ptaoussanis) so that I can improve these docs!
[1] `java.time.Instant` or `js/Date`
[2] `java.lang.Throwable` or `js/Error`
[3] Most Telemere signal content is evaluated CONDITIONALLY (iff signal allowed),
LAZILY (when signal is created), and on the HANDLING THREAD (not logging thread).
This allows efficient filtering, better control+monitoring of back pressure,
conditional effects, etc. Ref. <https://www.taoensso.com/telemere/flow> for visual!

View file

@ -0,0 +1,84 @@
"Spy" signal creator, emphasizing (optional level) + form to run.
Default kind: `:spy`
Default level: `:info`
Returns: ALWAYS (unconditionally) returns run value, or rethrows run error.
When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to
console/file/queue/db, etc.).
Enables tracing of given `run` form:
- Resulting signal will include {:keys [run-form run-val run-nsecs]}.
- Nested signals will include this signal's id and uid under `:parent`.
Limitations:
1. Traced `run` form is usually expected to be synchronous and eager.
So no lazy seqs, async calls, or inversion of flow control (IoC) macros like
core.async `go` blocks, etc.
2. Tracing call (`spy!`) is usually expected to occur *within* normally flowing code.
IoC macros can arbitrarily (and often opaquely) alter program flow and tracing
across flow boundaries can be fragile or even fundamentally illogical.
So use within IoC macro bodies might not make conceptual sense, or could produce
errors or unreliable/confusing results.
Basically- if possible, prefer tracing normal Clojure fns running within normal
Clojure fns unless you deeply understand what your IoC macros are up to.
Examples:
(spy! (+ 1 2)) ; %> {:kind :trace, :level :info, :run-form '(+ 1 2),
; :run-val 3, :run-nsecs <int>, :parent {:keys [id uid]}
; :msg "(+ 1 2) => 3" ...}
(spy! :debug (+ 1 2)) ; %> {... :level :debug ...}
(spy!
{:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}
:msg ["My message:" x]}
(+ 1 2)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
Tips:
- Test using `with-signal`: (with-signal (spy! ...)).
- Supports the same options [2] as other signals [1].
- Like `trace!`, but takes optional level rather than optional id.
- Useful for debugging/monitoring forms, and tracing (nested) execution flow.
- Execution of `run` form may create additional (nested) signals.
Each signal's `:parent` key will indicate its immediate parent.
- It's often useful to wrap `run` form with `catch->error!`:
(trace! ::trace-id (catch->error! ::error-id ...)).
This way you have independent filtering for `run` forms that throw,
allowing you to use a higher min level and/or reduced sampling, etc.
In this case you'll create:
0 or 1 `:trace` signals (depending on filtering), AND
0 or 1 `:error` signals (depending on filtering).
Note that the `:error` signal will contain tracing info (e.g. `:parent` key)
iff the enclosing `trace!` is allowed.
- Runtime of async or lazy code in `run` form will intentionally NOT be
included in resulting signal's `:run-nsecs` value. If you want to measure
such runtimes, make sure that your form wraps where the relevant costs are
actually realized. Compare:
(spy! (delay (my-slow-code))) ; Doesn't measure slow code
(spy! @(delay (my-slow-code))) ; Does measure slow code
- See also Tufte (https://www.taoensso.com/tufte) for a complementary/partner
Clj/s library that offers more advanced performance measurment and shares
the same signal engine (filtering and handler API) as Telemere.
----------------------------------------------------------------------
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
[2] See `help:signal-options` - {:keys [kind level id data ...]}
[3] See `help:signal-content` - {:keys [kind level id data ...]}
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)

View file

@ -0,0 +1,88 @@
"Trace" signal creator, emphasizing (optional id) + form to run.
Default kind: `:trace`
Default level: `:info` (intentionally NOT `:trace`!)
Returns: ALWAYS (unconditionally) returns run value, or rethrows run error.
When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to
console/file/queue/db, etc.).
Enables tracing of given `run` form:
- Resulting signal will include {:keys [run-form run-val run-nsecs]}.
- Nested signals will include this signal's id and uid under `:parent`.
Limitations:
1. Traced `run` form is usually expected to be synchronous and eager.
So no lazy seqs, async calls, or inversion of flow control (IoC) macros like
core.async `go` blocks, etc.
2. Tracing call (`trace!`) is usually expected to occur *within* normally flowing code.
IoC macros can arbitrarily (and often opaquely) alter program flow and tracing
across flow boundaries can be fragile or even fundamentally illogical.
So use within IoC macro bodies might not make conceptual sense, or could produce
errors or unreliable/confusing results.
Basically- if possible, prefer tracing normal Clojure fns running within normal
Clojure fns unless you deeply understand what your IoC macros are up to.
Examples:
(trace! (+ 1 2)) ; %> {:kind :trace, :level :info, :run-form '(+ 1 2),
; :run-val 3, :run-nsecs <int>, :parent {:keys [id uid]} ...
; :msg "(+ 1 2) => 3" ...}
(trace! ::my-id (+ 1 2)) ; %> {... :id ::my-id ...}
(trace!
{:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}
:msg ["My message:" x]}
(+ 1 2)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
Tips:
- Test using `with-signal`: (with-signal (trace! ...)).
- Supports the same options [2] as other signals [1].
- Like `spy!`, but takes optional id rather than optional level.
- Useful for debugging/monitoring forms, and tracing (nested) execution flow.
- Execution of `run` form may create additional (nested) signals.
Each signal's `:parent` key will indicate its immediate parent.
- It's often useful to wrap `run` form with `catch->error!`:
(trace! ::trace-id (catch->error! ::error-id ...)).
This way you have independent filtering for `run` forms that throw,
allowing you to use a higher min level and/or reduced sampling, etc.
In this case you'll create:
0 or 1 `:trace` signals (depending on filtering), AND
0 or 1 `:error` signals (depending on filtering).
Note that the `:error` signal will contain tracing info (e.g. `:parent` key)
iff the enclosing `trace!` is allowed.
- Default level is `:info`, not `:trace`! The name "trace" in "trace signal"
refers to the general action of tracing program flow rather than to the
common logging level of the same name.
- Runtime of async or lazy code in `run` form will intentionally NOT be
included in resulting signal's `:run-nsecs` value. If you want to measure
such runtimes, make sure that your form wraps where the relevant costs are
actually realized. Compare:
(trace! (delay (my-slow-code))) ; Doesn't measure slow code
(trace! @(delay (my-slow-code))) ; Does measure slow code
- See also Tufte (https://www.taoensso.com/tufte) for a complementary/partner
Clj/s library that offers more advanced performance measurment and shares
the same signal engine (filtering and handler API) as Telemere.
----------------------------------------------------------------------
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
[2] See `help:signal-options` - {:keys [kind level id data ...]}
[3] See `help:signal-content` - {:keys [kind level id data ...]}
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)

View file

@ -0,0 +1,557 @@
(ns taoensso.telemere
"Structured telemetry for Clojure/Script applications.
See the GitHub page (esp. Wiki) for info on motivation and design:
<https://www.taoensso.com/telemere>"
{:author "Peter Taoussanis (@ptaoussanis)"}
(:refer-clojure :exclude [newline])
(:require
[taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.encore.signals :as sigs]
[taoensso.telemere.impl :as impl]
[taoensso.telemere.utils :as utils]
#?(:default [taoensso.telemere.consoles :as consoles])
#?(:clj [taoensso.telemere.streams :as streams])
#?(:clj [taoensso.telemere.files :as files]))
#?(:cljs
(:require-macros
[taoensso.telemere :refer
[with-signal with-signals signal-allowed?
signal! event! log! trace! spy! catch->error!
;; Via `sigs/def-api`
without-filters with-kind-filter with-ns-filter with-id-filter
with-min-level with-handler with-handler+
with-ctx with-ctx+ with-xfn with-xfn+]])))
(comment
(remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview)))
(enc/assert-min-encore-version [3 159 0])
;;;; Shared signal API
(declare ; Needed to avoid `clj-kondo` "Unresolved var" warnings
level-aliases
help:filters help:handlers help:handler-dispatch-options
get-filters get-min-levels get-handlers get-handlers-stats
#?(:clj without-filters)
set-kind-filter! #?(:clj with-kind-filter)
set-ns-filter! #?(:clj with-ns-filter)
set-id-filter! #?(:clj with-id-filter)
set-min-level! #?(:clj with-min-level)
#?(:clj with-handler) #?(:clj with-handler+)
add-handler! remove-handler! stop-handlers!
with-signal with-signals
^:dynamic *ctx* set-ctx! #?(:clj with-ctx) #?(:clj with-ctx+)
^:dynamic *xfn* set-xfn! #?(:clj with-xfn) #?(:clj with-xfn+))
(def default-handler-dispatch-opts
"See `help:handler-dispatch-opts` for details."
(dissoc sigs/default-handler-dispatch-opts
:convey-bindings? ; We use `enc/bound-delay`
))
(sigs/def-api
{:sf-arity 4
:ct-call-filter impl/ct-call-filter
:*rt-call-filter* impl/*rt-call-filter*
:*sig-handlers* impl/*sig-handlers*
:lib-dispatch-opts default-handler-dispatch-opts})
;;;; Aliases
(enc/defaliases
;; Encore
#?(:clj ^:no-doc enc/set-var-root!)
#?(:clj ^:no-doc enc/update-var-root!)
#?(:clj enc/get-env)
#?(:clj enc/call-on-shutdown!)
^:no-doc enc/chance
enc/rate-limiter
^:no-doc enc/newline
sigs/comp-xfn
#?(:clj truss/keep-callsite)
;; Impl
impl/msg-splice
impl/msg-skip
#?(:clj impl/with-signal)
#?(:clj impl/with-signals)
;; Utils
utils/clean-signal-fn
utils/format-signal-fn
utils/pr-signal-fn
utils/error-signal?)
;;;; Help
(do
(impl/defhelp help:signal-creators :signal-creators)
(impl/defhelp help:signal-options :signal-options)
(impl/defhelp help:signal-content :signal-content)
(impl/defhelp help:environmental-config :environmental-config))
;;;; Unique ids
(def ^:dynamic *uid-fn*
"Experimental, subject to change. Feedback welcome!
(fn [root?]) used to generate signal `:uid` values (unique instance ids)
when tracing.
Relevant only when `otel-tracing?` is false.
If `otel-tracing?` is true, uids are instead generated by `*otel-tracer*`.
`root?` argument is true iff signal is a top-level trace (i.e. form being
traced is unnested = has no parent form). Root-level uids typically need
more entropy and so are usually longer (e.g. 32 vs 16 hex chars).
Override default by setting one of the following:
1. JVM property: `taoensso.telemere.uid-kind`
2. Env variable: `TAOENSSO_TELEMERE_UID_KIND`
3. Classpath resource: `taoensso.telemere.uid-kind`
Possible (compile-time) values include:
`:uuid` - UUID string (Cljs) or `java.util.UUID` (Clj)
`:uuid-str` - UUID string (36/36 chars)
`:nano/secure` - nano-style string (21/10 chars) w/ strong RNG
`:nano/insecure` - nano-style string (21/10 chars) w/ fast RNG (default)
`:hex/insecure` - hex-style string (32/16 chars) w/ strong RNG
`:hex/secure` - hex-style string (32/16 chars) w/ fast RNG"
(utils/parse-uid-fn impl/uid-kind))
(comment (enc/qb 1e6 (*uid-fn* true) (*uid-fn* false))) ; [79.4 63.53]
;;;; OpenTelemetry
#?(:clj
(def otel-tracing?
"Experimental, subject to change. Feedback welcome!
Should Telemere's tracing signal creators (`trace!`, `spy!`, etc.)
interop with OpenTelemetry Java [1]? This will affect relevant
Telemere macro expansions.
Defaults to `true` iff OpenTelemetry Java is present when this
namespace is evaluated/compiled.
If `false`:
1. Telemere's OpenTelemetry handler will NOT emit to `SpanExporter`s.
2. Telemere and OpenTelemetry will NOT recognize each other's spans.
If `true`:
1. Telemere's OpenTelemetry handler WILL emit to `SpanExporter`s.
2. Telemere and OpenTelemetry WILL recognize each other's spans.
Override default by setting one of the following to \"true\" or \"false\":
1. JVM property: `taoensso.telemere.otel-tracing`
2. Env variable: `TAOENSSO_TELEMERE_OTEL_TRACING`
3. Classpath resource: `taoensso.telemere.otel-tracing`
See also: `otel-default-providers_`, `*otel-tracer*`,
`taoensso.telemere.open-telemere/handler:open-telemetry`.
[1] Ref. <https://github.com/open-telemetry/opentelemetry-java>"
impl/enabled:otel-tracing?))
#?(:clj
(def otel-default-providers_
"Experimental, subject to change. Feedback welcome!
When OpenTelemetry Java API [1] is present, value will be a delayed map
with keys:
:logger-provider - default `io.opentelemetry.api.logs.LoggerProvider`
:tracer-provider - default `io.opentelemetry.api.trace.TracerProvider`
:via - ∈ #{:sdk-extension-autoconfigure :global}
:auto-configured-sdk - `io.opentelemetry.sdk.OpenTelemetrySdk` or nil
Uses `AutoConfiguredOpenTelemetrySdk` when possible, or
`GlobalOpenTelemetry` otherwise.
See the relevant OpenTelemetry Java docs for details.
[1] Ref. <https://github.com/open-telemetry/opentelemetry-java>"
(enc/compile-when impl/present:otel?
(delay
(or
;; Via SDK autoconfiguration extension (when available)
(enc/compile-when
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
(truss/catching :common
(let [builder (io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk/builder)
sdk (.getOpenTelemetrySdk (.build builder))]
{:logger-provider (.getLogsBridge sdk)
:tracer-provider (.getTracerProvider sdk)
:via :sdk-extension-autoconfigure
:auto-configured-sdk sdk})))
;; Via Global (generally not recommended)
(let [g (io.opentelemetry.api.GlobalOpenTelemetry/get)]
{:logger-provider (.getLogsBridge g)
:tracer-provider (.getTracerProvider g)
:via :global}))))))
#?(:clj
(def ^:dynamic *otel-tracer*
"Experimental, subject to change. Feedback welcome!
OpenTelemetry `Tracer` to use for Telemere's tracing signal creators
(`trace!`, `span!`, etc.), ∈ #{nil io.opentelemetry.api.trace.Tracer Delay}.
Defaults to the provider in `otel-default-providers_`.
See also `otel-tracing?`."
(enc/compile-when impl/enabled:otel-tracing?
(delay
(when-let [^io.opentelemetry.api.trace.TracerProvider p
(get (force otel-default-providers_) :tracer-provider)]
(do #_impl/viable-tracer (.get p "Telemere")))))))
(comment (enc/qb 1e6 (force *otel-tracer*))) ; 51.23
;;;; Signal creators
;; - log! ------------- ?level + msg => nil
;; - event! ----------- id + ?level => nil
;; - trace! ----------- ?id + run => run result (value or throw)
;; - spy! ------------- ?level + run => run result (value or throw)
;; - error! ----------- ?id + error => given error
;; - catch->error! ---- ?id + run => run value or ?catch-val
;; - uncaught->error! - ?id => nil
;; - signal! ---------- opts => allowed? / run result (value or throw)
#?(:clj
(defn- args->opts [args]
(case (count args)
0 {}
1 (first args)
(apply hash-map args))))
#?(:clj
(defmacro signal-allowed?
"Returns true iff signal with given opts would meet filtering conditions:
(when (signal-allowed? {:level :warn, <...>}) (my-custom-code))
Allows you to use Telemere's rich filtering system for conditionally
executing arbitrary code. Also handy for batching multiple signals
under a single set of conditions (incl. sampling, rate limiting, etc.):
;; Logs exactly 2 or 0 messages (never 1):
(when (signal-allowed? {:level :info, :sample 0.5})
(log! {:allow? true} \"Message 1\")
(log! {:allow? true} \"Message 2\"))"
;; Used also for interop (tools.logging, SLF4J), etc.
{:arglists (impl/arglists :signal-allowed?)}
[& args]
(truss/keep-callsite
`(impl/signal-allowed? ~(args->opts args)))))
(comment (macroexpand '(signal-allowed? {:ns "my-ns"})))
#?(:clj
(defmacro signal!
"opts => allowed? / run result (value or throw)."
{:doc (impl/docstring :signal!)
:arglists (impl/arglists :signal!)}
[& args]
(truss/keep-callsite
`(impl/signal! ~(args->opts args)))))
(comment (:coords (macroexpand '(with-signal (signal!)))))
#?(:clj
(defn- merge-or-assoc-opts [m macro-form k v]
(let [m (assoc m :coords (truss/callsite-coords macro-form))]
(if (map? v)
(merge m v)
(assoc m k v)))))
#?(:clj
(let [base-opts {:kind :log, :level :info}]
(defmacro log!?
"?level + msg => allowed?"
{:doc (impl/docstring :log!)
:arglists (impl/arglists :log!)}
([opts-or-msg ] `(impl/signal! ~(merge-or-assoc-opts base-opts &form :msg opts-or-msg)))
([opts-or-level msg] `(impl/signal! ~(assoc (merge-or-assoc-opts base-opts &form :level opts-or-level) :msg msg))))))
(comment (:coords (with-signal (log!? :info "My msg"))))
#?(:clj
(defmacro log!
"Like `log!?` but always returns nil."
{:doc (impl/docstring :log!)
:arglists (impl/arglists :log!)}
[& args] `(do ~(truss/keep-callsite `(log!? ~@args)) nil)))
(comment (:coords (with-signal (log! :info "My msg"))))
#?(:clj
(let [base-opts {:kind :event, :level :info}]
(defmacro event!?
"id + ?level => allowed? Note unique arg order: [x opts] rather than [opts x]!"
{:doc (impl/docstring :event!)
:arglists (impl/arglists :event!)}
([ opts-or-id] `(impl/signal! ~(merge-or-assoc-opts base-opts &form :id opts-or-id)))
([id opts-or-level] `(impl/signal! ~(assoc (merge-or-assoc-opts base-opts &form :level opts-or-level) :id id))))))
(comment (:coords (with-signal (event!? ::my-id :info))))
#?(:clj
(defmacro event!
"Like `event!?` but always returns nil."
{:doc (impl/docstring :event!)
:arglists (impl/arglists :event!)}
[& args] `(do ~(truss/keep-callsite `(event!? ~@args)) nil)))
(comment (:coords (with-signal (event! ::my-id :info))))
#?(:clj
(let [base-opts {:kind :trace, :level :info, :msg `impl/default-trace-msg}]
(defmacro trace!
"?id + run => run result (value or throw)."
{:doc (impl/docstring :trace!)
:arglists (impl/arglists :trace!)}
([opts-or-run] `(impl/signal! ~(merge-or-assoc-opts base-opts &form :run opts-or-run)))
([opts-or-id run] `(impl/signal! ~(assoc (merge-or-assoc-opts base-opts &form :id opts-or-id) :run run))))))
(comment (:coords (with-signal (trace! ::my-id (+ 1 2)))))
#?(:clj
(let [base-opts {:kind :spy, :level :info, :msg `impl/default-trace-msg}]
(defmacro spy!
"?level + run => run result (value or throw)."
{:doc (impl/docstring :spy!)
:arglists (impl/arglists :spy!)}
([opts-or-run] `(impl/signal! ~(merge-or-assoc-opts base-opts &form :run opts-or-run)))
([opts-or-level run] `(impl/signal! ~(assoc (merge-or-assoc-opts base-opts &form :level opts-or-level) :run run))))))
(comment (with-signals (spy! :info (+ 1 2))))
#?(:clj
(let [base-opts {:kind :error, :level :error}]
(defmacro error!
"?id + error => given error."
{:doc (impl/docstring :error!)
:arglists (impl/arglists :error!)}
([opts-or-id error] `(error! ~(assoc (merge-or-assoc-opts base-opts &form :id opts-or-id) :error error)))
([opts-or-error]
(let [opts (merge-or-assoc-opts base-opts &form :error opts-or-error)
gs-error (gensym "error")]
`(let [~gs-error ~(get opts :error)]
(impl/signal! ~(assoc opts :error gs-error))
~gs-error))))))
(comment (:coords (with-signal (throw (error! ::my-id (truss/ex-info "MyEx" {}))))))
#?(:clj
(let [base-opts {:kind :error, :level :error}]
(defmacro catch->error!
"?id + run => run value or ?catch-val."
{:doc (impl/docstring :catch->error!)
:arglists (impl/arglists :catch->error!)}
([opts-or-id run] `(catch->error! ~(assoc (merge-or-assoc-opts base-opts &form :id opts-or-id) :run run)))
([opts-or-run]
(let [opts (merge-or-assoc-opts base-opts &form :run opts-or-run)
rethrow? (not (contains? opts :catch-val))
catch-val (get opts :catch-val)
run-form (get opts :run)
opts (dissoc opts :run :catch-val)
gs-caught (gensym "caught")]
`(truss/try* ~run-form
(catch :all ~gs-caught
(impl/signal! ~(assoc opts :error gs-caught))
(if ~rethrow? (throw ~gs-caught) ~catch-val))))))))
(comment (:coords (with-signal (catch->error! ::my-id (/ 1 0)))))
#?(:clj
(defn uncaught->handler!
"Sets JVM's global `DefaultUncaughtExceptionHandler` to given
(fn handler [`<java.lang.Thread>` `<java.lang.Throwable>`]).
See also `uncaught->error!`."
[handler]
(Thread/setDefaultUncaughtExceptionHandler
(when handler ; falsey to remove
(reify Thread$UncaughtExceptionHandler
(uncaughtException [_ thread throwable]
(handler thread throwable)))))
nil))
#?(:clj
(let [base-opts
{:kind :error, :level :error,
:msg `["Uncaught Throwable on thread:" (.getName ~(with-meta '__thread-arg {:tag 'java.lang.Thread}))]
:error '__throwable-arg}]
(defmacro uncaught->error!
"Uses `uncaught->handler!` so that `error!` will be called for
uncaught JVM errors.
See `uncaught->handler!` and `error!` for details."
{:arglists (impl/arglists :uncaught->error!)}
([ ] (truss/keep-callsite `(uncaught->error! {})))
([opts-or-id]
(let [opts (merge-or-assoc-opts base-opts &form :id opts-or-id)]
`(uncaught->handler!
(fn [~'__thread-arg ~'__throwable-arg]
(impl/signal! ~opts))))))))
(comment
(macroexpand '(uncaught->error! ::uncaught))
(do
(uncaught->error! ::uncaught)
(enc/threaded :user (/ 1 0))))
;;;;
(defn dispatch-signal!
"Dispatches given signal to registered handlers, supports `with-signal/s`.
Normally called automatically (internally) by signal creators, this util
is provided publicly since it's also handy for manually re/dispatching
custom/modified signals, etc.:
(let [original-signal (with-signal :trap (event! ::my-id1))
modified-signal (assoc original-signal :id ::my-id2)]
(dispatch-signal! modified-signal))"
[signal]
(when-let [wrapped-signal (impl/wrap-signal signal)]
(impl/dispatch-signal! wrapped-signal)))
(comment (dispatch-signal! (assoc (with-signal :trap (log! "hello")) :level :warn)))
;;;; Interop
#?(:clj
(enc/defaliases
impl/check-interop
streams/with-out->telemere
streams/with-err->telemere
streams/with-streams->telemere
streams/streams->telemere!
streams/streams->reset!))
(comment (check-interop))
;;;; Handlers
(enc/defaliases
#?(:default consoles/handler:console)
#?(:cljs consoles/handler:console-raw)
#?(:clj files/handler:file))
;;;; Init
(impl/on-init
(enc/set-var-root! sigs/*default-handler-error-fn*
(fn [{:keys [error] :as m}]
(impl/signal!
{:kind :error
:level :error
:error error
:ns "taoensso.encore.signals"
:id :taoensso.encore.signals/handler-error
:msg "Error executing wrapped handler fn"
:data (dissoc m :error)})))
(enc/set-var-root! sigs/*default-handler-backp-fn*
(fn [data]
(impl/signal!
{:kind :event
:level :warn
:ns "taoensso.encore.signals"
:id :taoensso.encore.signals/handler-back-pressure
:msg "Back pressure on wrapped handler fn"
:data data})))
(add-handler! :default/console (handler:console) {:async nil})
#?(:clj (truss/catching (require '[taoensso.telemere.tools-logging]))) ; TL->Telemere
#?(:clj (truss/catching (require '[taoensso.telemere.slf4j]))) ; SLF4J->Telemere
#?(:clj (truss/catching (require '[taoensso.telemere.open-telemetry]))) ; Telemere->OTel
)
;;;; Flow benchmarks
(comment
{:last-updated "2024-08-15"
:system "2020 Macbook Pro M1, 16 GB memory"
:clojure-version "1.12.0-rc1"
:java-version "OpenJDK 22"}
[(binding [impl/*sig-handlers* nil]
(enc/qb 1e6 ; [9.31 16.76 264.12 350.43]
(signal! {:level :info, :run nil, :elide? true }) ; 9
(signal! {:level :info, :run nil, :allow? false}) ; 17
(signal! {:level :info, :run nil, :allow? true }) ; 264
(signal! {:level :info, :run nil }) ; 350
))
(binding [impl/*sig-handlers* nil]
(enc/qb 1e6 ; [8.34 15.78 999.27 444.08 1078.83]
(signal! {:level :info, :run "run", :elide? true }) ; 8
(signal! {:level :info, :run "run", :allow? false}) ; 16
(signal! {:level :info, :run "run", :allow? true }) ; 1000
(signal! {:level :info, :run "run", :trace? false}) ; 444
(signal! {:level :info, :run "run" }) ; 1079
))
;; For README "performance" table
(binding [impl/*sig-handlers* nil]
(enc/qb [8 1e6] ; [9.34 347.7 447.71 1086.65]
(signal! {:level :info, :elide? true }) ; 9
(signal! {:level :info }) ; 348
(signal! {:level :info, :run "run", :trace? false}) ; 448
(signal! {:level :info, :run "run" }) ; 1087
))
;; Full bench to handled signals
;; Sync => 4240.6846 (~4.2m/sec)
;; Async dropping => 2421.9176 (~2.4m/sec)
(let [runtime-msecs 5000
n-procs (.availableProcessors (Runtime/getRuntime))
fp (enc/future-pool n-procs)
c (java.util.concurrent.atomic.AtomicLong. 0)
p (promise)]
(with-handler ::bench (fn [_] (.incrementAndGet c))
{:async nil} ; Sync
#_{:async {:mode :dropping, :n-threads n-procs}}
(let [t (enc/after-timeout runtime-msecs (deliver p (.get c)))]
(dotimes [_ n-procs]
(fp (fn [] (dotimes [_ 6e6] (signal! {:level :info})))))
(/ (double @p) (double runtime-msecs)))))])
;;;;
(comment
(with-handler :hid1 (handler:console) {} (log! "Message"))
(let [sig
(with-signal
(event! ::ev-id
{:data {:a :A :b :b}
:error
(truss/ex-info "Ex2" {:b :B}
(truss/ex-info "Ex1" {:a :A}))}))]
(do (let [hf (handler:file)] (hf sig) (hf)))
(do (let [hf (handler:console)] (hf sig) (hf)))
#?(:cljs (let [hf (handler:console-raw)] (hf sig) (hf)))))
(comment (let [{[s1 s2] :signals} (with-signals (trace! ::id1 (trace! ::id2 "form2")))] s1))

View file

@ -1,19 +1,18 @@
(ns ^:no-doc taoensso.telemere.consoles (ns ^:no-doc taoensso.telemere.consoles
"Private ns, implementation detail. "Telemere -> console handlers."
Core console handlers, aliased in main Telemere ns."
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.utils :as utils])) [taoensso.telemere.utils :as utils]))
(comment (comment
(require '[taoensso.telemere :as tel]) (require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.consoles) (remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview))) (:api (enc/interns-overview)))
#?(:clj #?(:clj
(defn ^:public handler:console (defn ^:public handler:console
"Experimental, subject to change. "Alpha, subject to change.
Returns a signal handler that: Returns a signal handler that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Writes the signal as a string to specified stream. - Writes the signal as a string to specified stream.
@ -23,11 +22,11 @@
Options: Options:
`:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn` `:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
`:stream` - `java.io.writer` `:stream` ---- `java.io.writer`
Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise." Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise."
([] (handler:console nil)) ([] (handler:console nil))
([{:keys [stream output-fn ] ([{:keys [stream output-fn]
:or :or
{stream :auto {stream :auto
output-fn (utils/format-signal-fn)}}] output-fn (utils/format-signal-fn)}}]
@ -39,8 +38,8 @@
([signal] ([signal]
(let [^java.io.Writer stream (let [^java.io.Writer stream
(case stream (case stream
:*out* *out* (:out :*out*) *out*
:*err* *err* (:err :*err*) *err*
:auto (if (error-signal? signal) *err* *out*) :auto (if (error-signal? signal) *err* *out*)
stream)] stream)]
@ -50,8 +49,7 @@
:cljs :cljs
(defn ^:public handler:console (defn ^:public handler:console
"Experimental, subject to change. "Alpha, subject to change.
If `js/console` exists, returns a signal handler that: If `js/console` exists, returns a signal handler that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Writes the signal as a string to JavaScript console. - Writes the signal as a string to JavaScript console.
@ -87,8 +85,7 @@
#?(:cljs #?(:cljs
(defn ^:public handler:console-raw (defn ^:public handler:console-raw
"Experimental, subject to change. "Alpha, subject to change.
If `js/console` exists, returns a signal handler that: If `js/console` exists, returns a signal handler that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Writes the raw signal to JavaScript console. - Writes the raw signal to JavaScript console.
@ -97,8 +94,10 @@
Ref. <https://github.com/binaryage/cljs-devtools>. Ref. <https://github.com/binaryage/cljs-devtools>.
Options: Options:
`:preamble-fn` - (fn [signal]) => string. `:preamble-fn` ----- (fn [signal]) => string, see [1].
`:format-nsecs-fn` - (fn [nanosecs]) => string." `:format-nsecs-fn` - (fn [nanosecs]) => string.
[1] `taoensso.telemere.utils/signal-preamble-fn`, etc."
([] (handler:console-raw nil)) ([] (handler:console-raw nil))
([{:keys [preamble-fn format-nsecs-fn] :as opts ([{:keys [preamble-fn format-nsecs-fn] :as opts
@ -124,7 +123,7 @@
(.group js/console (preamble-fn signal)) (.group js/console (preamble-fn signal))
(content-fn signal (logger-fn logger) identity) (content-fn signal (logger-fn logger) identity)
(when-let [stack (and error (.-stack (enc/ex-root error)))] (when-let [stack (and error (.-stack (truss/ex-root error)))]
(.call logger logger stack)) (.call logger logger stack))
(.groupEnd js/console))))))))) (.groupEnd js/console)))))))))

View file

@ -1,13 +1,13 @@
(ns ^:no-doc taoensso.telemere.files (ns ^:no-doc taoensso.telemere.files
"Private ns, implementation detail. "Telemere -> file handler."
Core file handler, aliased in main Telemere ns."
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.utils :as utils])) [taoensso.telemere.utils :as utils]))
(comment (comment
(require '[taoensso.telemere :as tel]) (require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.files) (remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview))) (:api (enc/interns-overview)))
;;;; Implementation ;;;; Implementation
@ -71,9 +71,9 @@
:daily (str (.format dtf (java.time.LocalDate/ofEpochDay edy)) "d") :daily (str (.format dtf (java.time.LocalDate/ofEpochDay edy)) "d")
:weekly (str (.format dtf (java.time.LocalDate/ofEpochDay (edy-week edy))) "w") :weekly (str (.format dtf (java.time.LocalDate/ofEpochDay (edy-week edy))) "w")
:monthly (str (.format dtf (java.time.LocalDate/ofEpochDay (edy-month edy))) "m") :monthly (str (.format dtf (java.time.LocalDate/ofEpochDay (edy-month edy))) "m")
(enc/unexpected-arg! interval (truss/unexpected-arg! interval
{:context `file-timestamp {:param 'interval
:param 'interval :context `file-timestamp
:expected #{:daily :weekly :monthly}})))) :expected #{:daily :weekly :monthly}}))))
(comment (file-timestamp->edy (format-file-timestamp :weekly (udt->edy (enc/now-udt*))))) (comment (file-timestamp->edy (format-file-timestamp :weekly (udt->edy (enc/now-udt*)))))
@ -81,7 +81,7 @@
(defn manage-test-files! (defn manage-test-files!
"Describes/creates/deletes files used for tests/debugging, etc." "Describes/creates/deletes files used for tests/debugging, etc."
[action] [action]
(have? [:el #{:return :println :create :delete}] action) (truss/have? [:el #{:return :println :create :delete}] action)
(let [fnames_ (volatile! []) (let [fnames_ (volatile! [])
action! action!
(fn [app timestamp part gz? timestamp main?] (fn [app timestamp part gz? timestamp main?]
@ -136,7 +136,7 @@
- Have the same `interval` type #{:daily :weekly :monthly nil} (=> ?timestamped). - Have the same `interval` type #{:daily :weekly :monthly nil} (=> ?timestamped).
- Have the given timestamp (e.g. \"2020-01-01d\", or nil for NO timestamp)." - Have the given timestamp (e.g. \"2020-01-01d\", or nil for NO timestamp)."
[main-path interval timestamp sort?] [main-path interval timestamp sort?]
(have? [:el #{:daily :weekly :monthly nil}] interval) (truss/have? [:el #{:daily :weekly :monthly nil}] interval)
(let [main-file (utils/as-file main-path) ; `logs/app.log` (let [main-file (utils/as-file main-path) ; `logs/app.log`
main-dir (.getParentFile (.getAbsoluteFile main-file)) ; `.../logs` main-dir (.getParentFile (.getAbsoluteFile main-file)) ; `.../logs`
@ -168,9 +168,8 @@
(let [actual (.getAbsolutePath file-in) (let [actual (.getAbsolutePath file-in)
expected file-name] expected file-name]
(when-not (.endsWith actual expected) (when-not (.endsWith actual expected)
(throw (truss/ex-info! "Unexpected file name"
(ex-info "Unexpected file name" {:actual actual, :expected expected})))
{:actual actual, :expected expected}))))
(conj acc (conj acc
{:file file-in {:file file-in
@ -234,7 +233,7 @@
arch-file+gz (utils/as-file arch-file-name+gz) ; `logs/app.log.1.gz` or `logs/app.log-2020-01-01d.1.gz` arch-file+gz (utils/as-file arch-file-name+gz) ; `logs/app.log.1.gz` or `logs/app.log-2020-01-01d.1.gz`
] ]
(have? false? (.exists arch-file+gz)) ; No pre-existing `.1.gz` (truss/have? false? (.exists arch-file+gz)) ; No pre-existing `.1.gz`
(.renameTo main-file arch-file-gz) (.renameTo main-file arch-file-gz)
(.createNewFile main-file) (.createNewFile main-file)
@ -272,32 +271,32 @@
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Writes (appends) the signal as a string to file specified by `path`. - Writes (appends) the signal as a string to file specified by `path`.
Depending on options, archives may be maintained: Can output signals as human or machine-readable (edn, JSON) strings.
Depending on options, archive file/s may also be maintained:
- `logs/app.log.n.gz` (for nil `:interval`, non-nil `:max-file-size`) - `logs/app.log.n.gz` (for nil `:interval`, non-nil `:max-file-size`)
- `logs/app.log-YYYY-MM-DDd.n.gz` (for non-nil `:interval`) ; d=daily/w=weekly/m=monthly - `logs/app.log-YYYY-MM-DDd.n.gz` (for non-nil `:interval`) ; d=daily/w=weekly/m=monthly
Can output signals as human or machine-readable (edn, JSON) strings.
Example files with default options: Example files with default options:
`/logs/telemere.log` ; Current file `/logs/telemere.log` ; Current file (newest entries)
`/logs/telemere.log-2020-01-01m.1.gz` ; Archive for Jan 2020, part 1 (newest entries) `/logs/telemere.log-2020-01-01m.1.gz` ; Archive for Jan 2020, part 1 (newest entries)
... ...
`/logs/telemere.log-2020-01-01m.8.gz` ; Archive for Jan 2020, part 8 (oldest entries) `/logs/telemere.log-2020-01-01m.8.gz` ; Archive for Jan 2020, part 8 (oldest entries)
Options: Options:
`:output-fn`- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn` `:output-fn`- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
`:path` - Path string of the target output file (default `logs/telemere.log`) `:path` ----- Path string of the target output file (default `logs/telemere.log`)
`:interval` - #{nil :daily :weekly :monthly} (default `:monthly`) `:interval` - #{nil :daily :weekly :monthly} (default `:monthly`)
When non-nil, causes interval-based archives to be maintained. When non-nil, causes interval-based archives to be maintained.
`:max-file-size` #{nil <pos-int>} (default 4MB) `:max-file-size` - #{nil <pos-int>} (default 4MB)
When `path` file size > ~this many bytes, rotates old content to numbered archives. When `path` file size > ~this many bytes, rotates old content to numbered archives.
`:max-num-parts` #{nil <pos-int>} (default 8) `:max-num-parts` - #{nil <pos-int>} (default 8)
Maximum number of numbered archives to retain for any particular interval. Maximum number of numbered archives to retain for any particular interval.
`:max-num-intervals` #{nil <pos-int>} (default 6) `:max-num-intervals` - #{nil <pos-int>} (default 6)
Maximum number of intervals (days/weeks/months) to retain." Maximum number of intervals (days/weeks/months) to retain."
([] (handler:file nil)) ([] (handler:file nil))

View file

@ -0,0 +1,810 @@
(ns ^:no-doc taoensso.telemere.impl
"Private ns, implementation detail.
Signal design shared by: Telemere, Tufte, Timbre."
(:require
[clojure.set :as set]
[taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.encore.signals :as sigs])
#?(:cljs
(:require-macros
[taoensso.telemere.impl :refer [with-signal]])))
(comment
(remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview)))
#?(:clj
(enc/declare-remote
^:dynamic taoensso.telemere/*ctx*
^:dynamic taoensso.telemere/*xfn*
^:dynamic taoensso.telemere/*uid-fn*
^:dynamic taoensso.telemere/*otel-tracer*))
;;;; Config
#?(:clj
(do
(def present:tools-logging? (enc/have-resource? "clojure/tools/logging.clj"))
(def present:slf4j? (enc/compile-if org.slf4j.Logger true false))
(def present:telemere-slf4j? (enc/compile-if com.taoensso.telemere.slf4j.TelemereLogger true false))
(def present:otel? (enc/compile-if io.opentelemetry.context.Context true false))
(def enabled:tools-logging?
"Documented at `taoensso.telemere.tools-logging/tools-logging->telemere!`."
(enc/get-env {:as :bool, :default false} :clojure.tools.logging/to-telemere))
(def enabled:otel-tracing?
"Documented at `taoensso.telemere/otel-tracing?`."
(enc/get-env {:as :bool, :default present:otel?}
:taoensso.telemere/otel-tracing<.platform>))
(def enabled:incl-host-info? "Include `:host` info in signals by default?" (enc/get-env {:as :bool, :default true} :taoensso.telemere/incl-host-info))
(def enabled:incl-thread-info? "Include `:thread` info in signals by default?" (enc/get-env {:as :bool, :default true} :taoensso.telemere/incl-thread-info))))
(def uid-kind
"Documented at `taoensso.telemere/*uid-fn*`."
(enc/get-env {:as :edn, :default :default}
:taoensso.telemere/uid-kind<.platform><.edn>))
#?(:clj
(let [base (enc/get-env {:as :edn} :taoensso.telemere/ct-filters<.platform><.edn>)
kind-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-kind-filter<.platform><.edn>)
ns-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-ns-filter<.platform><.edn>)
id-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-id-filter<.platform><.edn>)
min-level (enc/get-env {:as :edn} :taoensso.telemere/ct-min-level<.platform><.edn>)]
(enc/defonce ct-call-filter
"`SpecFilter` used for compile-time elision, or nil."
(sigs/spec-filter
{:kind-filter (or kind-filter (get base :kind-filter))
:ns-filter (or ns-filter (get base :ns-filter))
:id-filter (or id-filter (get base :id-filter))
:min-level (or min-level (get base :min-level))}))))
(let [base (enc/get-env {:as :edn} :taoensso.telemere/rt-filters<.platform><.edn>)
kind-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-kind-filter<.platform><.edn>)
ns-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-ns-filter<.platform><.edn>)
id-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-id-filter<.platform><.edn>)
min-level (enc/get-env {:as :edn, :default :info} :taoensso.telemere/rt-min-level<.platform><.edn>)]
(enc/defonce ^:dynamic *rt-call-filter*
"`SpecFilter` used for runtime filtering, or nil."
(sigs/spec-filter
{:kind-filter (or kind-filter (get base :kind-filter))
:ns-filter (or ns-filter (get base :ns-filter))
:id-filter (or id-filter (get base :id-filter))
:min-level (or min-level (get base :min-level))})))
(comment (enc/get-env {:as :edn, :return :explain} :taoensso.telemere/rt-filters<.platform><.edn>))
;;;; Utils
#?(:clj
(defmacro on-init [& body]
(let [sym (with-meta '__on-init {:private true})
compiling? (if (:ns &env) false `*compile-files*)]
`(defonce ~sym (when-not ~compiling? ~@body nil)))))
(comment (macroexpand-1 '(on-init (println "foo"))))
;;;; Messages
(deftype MsgSkip [])
(deftype MsgSplice [args])
(def ^:public msg-skip
"For use within signal message vectors.
Special value that will be ignored (noop) when creating message.
Useful for conditionally skipping parts of message content, etc.:
(signal! {:msg [\"Hello\" (if <cond> <then> msg-skip) \"world\"] <...>}) or
(log! [\"Hello\" (if <cond> <then> msg-skip) \"world\"]), etc.
%> {:msg_ \"Hello world\" <...>}"
(MsgSkip.))
(defn ^:public msg-splice
"For use within signal message vectors.
Wraps given arguments so that they're spliced when creating message.
Useful for conditionally splicing in extra message content, etc.:
(signal! {:msg [(when <cond> (msg-splice [\"Username:\" \"Steve\"])) <...>]}) or
(log! [(when <cond> (msg-splice [\"Username:\" \"Steve\"]))])
%> {:msg_ \"Username: Steve\"}"
[args] (MsgSplice. args))
(let [;; xform (map #(if (nil? %) "nil" %))
xform
(fn [rf]
(let [;; Protocol-based impln (extensible but ~20% slower)
;; rf* (fn rf* [acc in] (reduce-msg-arg in acc rf))
rf*
(fn rf* [acc in]
(enc/cond
(instance? MsgSplice in) (reduce rf* acc (.-args ^MsgSplice in))
(instance? MsgSkip in) acc
(nil? in) (rf acc "nil")
:else (rf acc in)))]
(fn
([ ] (rf))
([acc ] (rf acc))
([acc in] (rf* acc in)))))]
(defn signal-msg
"Returns string formed by joining all args with \" \" separator,
rendering nils as \"nil\". Supports `msg-skip`, `msg-splice`.
API intended to be usefully different to `str`:
- `str`: no spacers, skip nils, no splicing
- `signal-msg`: auto spacers, show nils, opt-in splicing"
{:tag #?(:clj 'String :cljs 'string)}
[args] (enc/str-join " " xform args)))
(comment
(enc/qb 2e6 ; [305.61 625.35]
(str "a" "b" "c" nil :kw) ; "abc:kw"
(signal-msg ["a" "b" "c" nil :kw (msg-splice ["d" "e"])]) ; "a b c nil :kw d e"
))
#?(:clj
(defn- parse-msg-form [msg-form]
(when msg-form
(enc/cond
(string? msg-form) msg-form
(vector? msg-form)
(enc/cond
(empty? msg-form) nil
:let [[m1 & more] msg-form]
(and (string? m1) (nil? more)) m1
:else `(delay (signal-msg ~msg-form)))
;; Auto delay-wrap (user should never delay-wrap!)
;; :else `(delay ~msg-form)
;; Leave user to delay-wrap when appropriate (document)
:else msg-form))))
(defn default-trace-msg
[form value error nsecs]
(if error
(str (if (nil? form) "nil" form) " !> " (truss/ex-type error))
(str (if (nil? form) "nil" form) " => " (if (nil? value) "nil" value))))
(comment
(default-trace-msg "(+ 1 2)" 3 nil 12345)
(default-trace-msg "(+ 1 2)" nil (Exception. "Ex") 12345))
;;;; Tracing
(enc/def* ^:dynamic *trace-root* "?{:keys [id uid]}" nil) ; Fixed once bound
(enc/def* ^:dynamic *trace-parent* "?{:keys [id uid]}" nil) ; Changes each nesting level
;; Root Telemere ids: {:parent nil, :id id1, :uid uid1 :root {:id id1, :uid uid1}}
;; Root OTel ids: {:parent nil, :id id1, :uid span1,:root {:id id1, :uid trace1}}
;;;; OpenTelemetry
#?(:clj
(enc/compile-when present:otel?
(do
(enc/def* ^:dynamic *otel-context* "`?Context`" nil)
(defmacro otel-context [] `(or *otel-context* (io.opentelemetry.context.Context/current)))
(defn otel-trace-id
"Returns valid `traceId` or nil."
[^io.opentelemetry.context.Context context]
(let [sc (.getSpanContext (io.opentelemetry.api.trace.Span/fromContext context))]
(when (.isValid sc) (.getTraceId sc))))
(defn otel-span-id
"Returns valid `spanId` or nil."
[^io.opentelemetry.context.Context context]
(let [sc (.getSpanContext (io.opentelemetry.api.trace.Span/fromContext context))]
(when (.isValid sc) (.getSpanId sc))))
(defn viable-tracer
"Returns viable `Tracer`, or nil."
[tracer]
(when-let [tracer ^io.opentelemetry.api.trace.Tracer tracer]
(let [sb (.spanBuilder tracer "test-span")
span (.startSpan sb)]
(when (.isValid (.getSpanContext span))
tracer))))
(def ^String otel-name (enc/fmemoize (fn [id] (if id (enc/as-qname id) "telemere/no-id"))))
(defn otel-context+span
"Returns new `Context` that includes minimal `Span` in given parent `Context`.
We leave the (expensive) population of attributes, etc. for signal handler.
Interop needs only the basics (t0, traceId, spanId, spanName) right away."
^io.opentelemetry.context.Context
[id inst ?parent-context ?span-kind]
(let [parent-context (or ?parent-context (otel-context))]
(enc/if-not [tracer (force taoensso.telemere/*otel-tracer*)]
parent-context ; Can't add Span without Tracer
(let [sb (.spanBuilder ^io.opentelemetry.api.trace.Tracer tracer (otel-name id))]
(.setStartTimestamp sb ^java.time.Instant inst)
(.setSpanKind sb
(case ?span-kind
(nil :internal) io.opentelemetry.api.trace.SpanKind/INTERNAL
:client io.opentelemetry.api.trace.SpanKind/CLIENT
:server io.opentelemetry.api.trace.SpanKind/SERVER
:consumer io.opentelemetry.api.trace.SpanKind/CONSUMER
:producer io.opentelemetry.api.trace.SpanKind/PRODUCER
(truss/unexpected-arg! ?span-kind
{:expected #{nil :internal :client :server :consumer :producer}})))
(.with ^io.opentelemetry.context.Context parent-context
(.startSpan sb)))))))))
(comment
(enc/qb 1e6 (otel-context) (otel-context+span ::id1 (enc/now-inst) nil nil)) ; [46.42 186.89]
(viable-tracer (force taoensso.telemere/*otel-tracer*))
(otel-trace-id (otel-context)))
;;;; Main types
(defrecord Signal
;; Telemere's main public data type, we avoid nesting and duplication
[schema inst uid, ns coords,
#?@(:clj [host thread _otel-context]),
sample, kind id level, ctx parent root, data kvs msg_,
error run-form run-val end-inst run-nsecs]
Object (toString [sig] (str "taoensso.telemere.Signal" (enc/pr-edn* (into {} sig)))))
;; Verbose constructors for readability + to support extra keys
(do (enc/def-print-impl [sig Signal] (str "#taoensso.telemere.Signal" (enc/pr-edn* (into {} sig)))))
#?(:clj (enc/def-print-dup [sig Signal] (str "#taoensso.telemere.impl.Signal" (enc/pr-edn* (into {} sig)))))
(defn signal? #?(:cljs {:tag 'boolean}) [x] (instance? Signal x))
(def impl-signal-keys #{:_otel-context})
(def standard-signal-keys
(set/difference (set (keys (map->Signal {:schema 0})))
impl-signal-keys))
(deftype #_defrecord WrappedSignal
[kind ns id level signal-value_]
sigs/ISignalHandling
(allow-signal? [_ spec-filter] (spec-filter kind ns id level))
(signal-debug [_] {:kind kind, :ns ns, :id id, :level level})
(signal-value [_ handler-sample-rate]
(sigs/signal-with-combined-sample-rate handler-sample-rate
(force signal-value_))))
(defn wrap-signal
"Used by `taoensso.telemere/dispatch-signal!`."
[signal]
(when (map? signal)
(let [{:keys [kind ns id level]} signal]
(WrappedSignal. kind ns id level signal))))
;;;; Handlers
(enc/defonce ^:dynamic *sig-handlers* "?[<wrapped-handler-fn>]" nil)
(defrecord SpyOpts [vol_ last-only? trap?])
(def ^:dynamic *sig-spy* "?SpyOpts" nil)
(defn force-msg-in-sig [sig]
(if-not (map? sig)
sig
(if-let [e (find sig :msg_)]
(assoc sig :msg_ (force (val e)))
(do sig))))
#?(:clj
(defmacro ^:public with-signal
"Executes given form, trapping errors. Returns the LAST signal created by form.
Useful for tests/debugging.
Options:
`trap-signals?` (default false)
Should ALL signals created by form be trapped to prevent normal dispatch
to registered handlers?
`raw-msg?` (default false)
Should delayed `:msg_` in returned signal be retained as-is?
Delay is otherwise replaced by realized string.
See also `with-signals` for more advanced options."
([ form] `(with-signal false false ~form))
([ trap-signals? form] `(with-signal false ~trap-signals? ~form))
([raw-msg? trap-signals? form]
`(let [sig_# (volatile! nil)]
(binding [*sig-spy* (SpyOpts. sig_# true ~trap-signals?)]
(truss/try* ~form (catch :all _#)))
(if ~raw-msg?
(do @sig_#)
(force-msg-in-sig @sig_#))))))
#?(:clj
(defmacro ^:public with-signals
"Like `with-signal` but returns {:keys [value error signals]}.
Useful for more advanced tests/debugging.
Destructuring example:
(let [{:keys [value error] [sig1 sig2] :signals} (with-signals ...)]
...)"
([ form] `(with-signals false false ~form))
([ trap-signals? form] `(with-signals false ~trap-signals? ~form))
([raw-msgs? trap-signals? form]
`(let [sigs_# (volatile! nil)
base-map#
(binding [*sig-spy* (SpyOpts. sigs_# false ~trap-signals?)]
(truss/try*
(do {:value ~form})
(catch :all t# {:error t#})))
sigs#
(not-empty
(if ~raw-msgs?
(do @sigs_#)
(mapv force-msg-in-sig @sigs_#)))]
(if sigs#
(assoc base-map# :signals sigs#)
(do base-map#))))))
#?(:clj (def ^:dynamic *sig-spy-off-thread?* false))
(defn dispatch-signal!
"Dispatches given signal to registered handlers, supports `with-signal/s`."
[signal]
(or
(when-let [{:keys [vol_ last-only? trap?]} *sig-spy*]
(let [sv
#?(:cljs (sigs/signal-value signal nil)
:clj
(if *sig-spy-off-thread?* ; Simulate async handler
(deref (enc/promised :user (sigs/signal-value signal nil)))
(do (sigs/signal-value signal nil))))]
(if last-only?
(vreset! vol_ sv)
(vswap! vol_ #(conj (or % []) sv))))
(when trap? :trapped))
(sigs/call-handlers! *sig-handlers* signal)
:dispatched))
;;;; API helpers
#?(:clj (defmacro docstring [ rname] (enc/slurp-resource (str "docs/" (name rname) ".txt"))))
#?(:clj (defmacro defhelp [sym rname] `(enc/def* ~sym {:doc ~(eval `(docstring ~rname))} "See docstring")))
#?(:clj
(defn arglists [macro-id]
;; + Undocumented [elide? allow? callsite-id host thread otel/context]
(case macro-id
:signal-allowed? ; opts => allowed?
'( [& opts-kvs]
[{:as opts-map :keys
[elidable? coords #_inst #_uid #_xfn #_xfn+,
sample kind ns id level when limit limit-by,
#_ctx #_ctx+ #_parent #_root #_trace?, #_do #_let #_data #_msg #_error #_run #_& #_kvs]}])
:signal! ; opts => allowed? / run result (value or throw)
'( [& opts-kvs]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error run & kvs]}])
:log! ; ?level + msg => nil / allowed?
'([opts-or-msg]
[level msg]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}
msg])
:event! ; id + ?level => nil / allowed?
'([opts-or-id]
[id level]
[id
{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}])
:trace! ; ?id + run => run result (value or throw)
'([opts-or-run]
[id run]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error run & kvs]}
run])
:spy! ; ?level + run => run result (value or throw)
'([opts-or-run]
[level run]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error run & kvs]}
run])
:error! ; ?id + error => given error
'([opts-or-error]
[id error]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}
error])
:catch->error! ; ?id + run => run value or ?catch-val
'([opts-or-run]
[id run]
[{:as opts-map :keys
[catch-val,
elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}
run])
:uncaught->error! ; ?id => nil
'([]
[opts-or-id]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}])
(truss/unexpected-arg! macro-id))))
;;;; Signal macro
(deftype RunResult [value error ^long run-nsecs]
#?(:clj clojure.lang.IFn :cljs IFn)
(#?(:clj invoke :cljs -invoke) [_] (if error (throw error) value))
(#?(:clj invoke :cljs -invoke) [_ signal_]
(if error
(truss/ex-info! "Signal `:run` form error"
(truss/try*
(do {:taoensso.telemere/signal (force signal_)})
(catch :all t {:taoensso.telemere/signal-error t}))
error)
value)))
(defn inst+nsecs
"Returns given platform instant plus given number of nanosecs."
[inst run-nsecs]
#?(:clj (.plusNanos ^java.time.Instant inst run-nsecs)
:cljs (js/Date. (+ (.getTime inst) (/ run-nsecs 1e6)))))
(comment (enc/qb 1e6 (inst+nsecs (enc/now-inst) 1e9)))
#?(:clj
(defn- valid-opts! [macro-form macro-env caller opts]
(if (map? opts)
(do opts)
(truss/ex-info!
(str "`" caller "` needs compile-time map opts at "
(sigs/format-callsite (enc/get-source macro-form macro-env)))))))
#?(:clj (defn- auto-> [form auto-form] (if (= form :auto) auto-form form)))
#?(:clj
(defmacro signal-allowed?
"Returns true iff signal with given opts would meet filtering conditions.
Wrapped for public API."
([ opts] (truss/keep-callsite `(signal-allowed? nil ~opts)))
([base-opts opts]
(valid-opts! &form &env 'telemere/signal-allowed? (or base-opts {}))
(valid-opts! &form &env 'telemere/signal-allowed? (or opts {}))
(let [opts (merge {:kind :generic, :level :info} base-opts opts)
{:keys [#_callsite-id elide? allow?]}
(sigs/filter-call
{:cljs? (boolean (:ns &env))
:sf-arity 4
:ct-call-filter ct-call-filter
:*rt-call-filter* `*rt-call-filter*}
(assoc opts
:ns (auto-> (get opts :ns :auto) (str *ns*))))]
(if elide? false `(if ~allow? true false))))))
(comment (macroexpand '(signal-allowed? {:level :info})))
#?(:clj
(defmacro signal!
"Generic low-level signal creator. Wrapped for public API."
([ opts] (truss/keep-callsite `(signal! nil ~opts)))
([base-opts opts]
(valid-opts! &form &env 'telemere/signal! (or base-opts {}))
(valid-opts! &form &env 'telemere/signal! (or opts {}))
(let [cljs? (boolean (:ns &env))
clj? (not cljs?)
opts (merge {:kind :generic, :level :info} base-opts opts)
run-form? (contains? opts :run)
run-form (get opts :run)
ns-form* (get opts :ns :auto)
ns-form (auto-> ns-form* (str *ns*))
show-run-val (get opts :run-val '_run-val)
show-run-form
(when run-form?
(get opts :run-form
(if (and
(enc/list-form? run-form)
(> (count run-form) 1)
(> (count (str run-form)) 32))
(list (first run-form) '...)
(do run-form))))
{:keys [#_callsite-id elide? allow?]}
(sigs/filter-call
{:cljs? cljs?
:sf-arity 4
:ct-call-filter ct-call-filter
:*rt-call-filter* `*rt-call-filter*}
(assoc opts
:ns ns-form
:local-forms
{:kind '__kind
:ns '__ns
:id '__id
:level '__level}))]
(if elide?
run-form
(let [coords (get opts :coords (when (= ns-form* :auto) (truss/callsite-coords &form)))
{inst-form :inst
kind-form :kind
id-form :id
level-form :level} opts
trace? (get opts :trace? run-form?)
_
(when-not (contains? #{true false nil} trace?)
(truss/ex-info!
(str "Signal needs compile-time `:trace?` value at "
(sigs/format-callsite ns-form coords))))
host-form (auto-> (get opts :host :auto) (when (and clj? enabled:incl-host-info?) `(enc/host-info)))
thread-form (auto-> (get opts :thread :auto) (when (and clj? enabled:incl-thread-info?) `(enc/thread-info)))
inst-form (auto-> (get opts :inst :auto) `(enc/now-inst*))
parent-form (get opts :parent `*trace-parent*)
root-form0 (get opts :root `*trace-root*)
uid-form (get opts :uid (when trace? :auto))
signal-delay-form
(let [{do-form :do
let-form :let
msg-form :msg
data-form :data
error-form :error
sample-form :sample} opts
let-form (or let-form '[])
msg-form (parse-msg-form msg-form)
ctx-form
(if-let [ctx+ (get opts :ctx+)]
`(taoensso.encore.signals/update-ctx taoensso.telemere/*ctx* ~ctx+)
(get opts :ctx `taoensso.telemere/*ctx*))
xfn-form
(if-let [xfn+ (get opts :xfn+)]
`(taoensso.encore.signals/comp-xfn taoensso.telemere/*xfn* ~xfn+)
(get opts :xfn `taoensso.telemere/*xfn*))
kvs-form
(let [base
(not-empty
(dissoc opts
:elidable? :coords :inst :uid :xfn :xfn+ :kvs+,
:sample :ns :kind :id :level :filter :when #_:limit #_:limit-by,
:ctx :ctx+ :parent :trace?, :do :let :data :msg :error,
:run :run-form :run-val, :elide? :allow? #_:callsite-id,
:host :thread :otel/context))]
(if-let [kvs+ (get opts :kvs+)] ; Undocumented
(if base
`(not-empty (conj ~base ~kvs+))
`(not-empty ~kvs+))
base))
_ ; Compile-time validation
(do
(when (and run-form? error-form) ; Ambiguous source of error
(truss/ex-info!
(str "Signal cannot have both `:run` and `:error` opts at "
(sigs/format-callsite ns-form coords))))
(when-let [e (find opts :msg_)] ; Common typo/confusion
(truss/ex-info!
(str "Signal cannot have `:msg_` opt (did you mean `:msg`?) at "
(sigs/format-callsite ns-form coords)))))
signal-form
(let [record-form
(let [clause [(if run-form? :run :no-run) (if clj? :clj :cljs)]]
(case clause
[:run :clj ] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~host-form ~'__thread ~'__otel-context, ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~'_msg_, ~'_run-err '~show-run-form ~show-run-val ~'_end-inst ~'_run-nsecs)
[:run :cljs] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~'_msg_, ~'_run-err '~show-run-form ~show-run-val ~'_end-inst ~'_run-nsecs)
[:no-run :clj ] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~host-form ~'__thread ~'__otel-context, ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~msg-form, ~error-form nil nil nil nil)
[:no-run :cljs] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~msg-form, ~error-form nil nil nil nil)
(truss/ex-info!
(str "Unexpected signal constructor args at "
(sigs/format-callsite ns-form coords)))))
record-form
(if-not run-form?
record-form
`(let [~(with-meta '_run-result {:tag `RunResult}) ~'__run-result
~'_run-nsecs (.-run-nsecs ~'_run-result)
~'_run-val (.-value ~'_run-result)
~'_run-err (.-error ~'_run-result)
~'_end-inst (inst+nsecs ~'__inst ~'_run-nsecs)
~'_msg_
(let [mf# ~msg-form]
(if (fn? mf#) ; Undocumented, handy for `trace!`/`spy!`, etc.
(delay (mf# '~show-run-form ~show-run-val ~'_run-err ~'_run-nsecs))
mf#))]
~record-form))]
(if-not kvs-form
record-form
`(let [signal# ~record-form]
(reduce-kv assoc signal# (.-kvs signal#)))))]
`(enc/bound-delay
;; Delay (cache) shared by all handlers, incl. `:let` eval,
;; signal construction, transform (xfn), etc. Throws caught by handler.
~do-form
(let [~@let-form ; Allow to throw, eval BEFORE data, msg, etc.
signal# ~signal-form]
;; Final unwrapped signal value visible to users/handler-fns, allow to throw
(if-let [xfn# ~xfn-form]
(xfn# signal#)
(do signal#)))))
;; Trade-off: avoid double `run-form` expansion
run-fn-form (when run-form? `(fn [] ~run-form))
run-form* (when run-form? `(~'__run-fn-form))
binds-form-base
`[~'__inst ~inst-form
~'__thread ~thread-form
~'__root0 ~root-form0 ; ?{:keys [id uid]}
~'__otel-context
~(when (and clj? enabled:otel-tracing?)
(if run-form?
`(otel-context+span ~'__id ~'__inst ~(get opts :otel/context `(otel-context)) ~(get opts :otel/span-kind))
(do (get opts :otel/context `(otel-context)))))
~'__uid
~(if (and clj? enabled:otel-tracing? trace?)
(auto-> uid-form `(or (otel-span-id ~'__otel-context) (com.taoensso.encore.Ids/genHexId16)))
(auto-> uid-form `(taoensso.telemere/*uid-fn* (if ~'__root0 false true))))]
binds-form-more
(enc/cond!
(not trace?) ; Non-tracing signal
`[~'__root1 ~'__root0 ; Retain, but don't establish
~'__run-result
~(when run-form?
`(let [t0# (enc/now-nano*)]
(truss/try*
(do (RunResult. ~run-form* nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#))))))]
;; Trace without OpenTelemetry
(or cljs? (not enabled:otel-tracing?))
`[~'__root1 (or ~'__root0 ~(when trace? `{:id ~'__id, :uid ~'__uid}))
~'__run-result
~(when run-form?
`(binding [*trace-root* ~'__root1
*trace-parent* {:id ~'__id, :uid ~'__uid}]
(let [t0# (enc/now-nano*)]
(truss/try*
(do (RunResult. ~run-form* nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#)))))))]
;; Trace with OpenTelemetry
(and clj? enabled:otel-tracing?)
`[~'__root1
(or ~'__root0
~(when trace?
`{:id ~'__id, :uid (or (otel-trace-id ~'__otel-context) (com.taoensso.encore.Ids/genHexId32))}))
~'__run-result
~(when run-form?
`(binding [*otel-context* ~'__otel-context
*trace-root* ~'__root1
*trace-parent* {:id ~'__id, :uid ~'__uid}]
(let [otel-scope# (.makeCurrent ~'__otel-context)
t0# (enc/now-nano*)]
(truss/try*
(do (RunResult. ~run-form* nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#)))
(finally (.close otel-scope#))))))])]
`((fn [] ; iife for better IoC compatibility
;; Unless otherwise specified, allow errors to throw on call
(let [~'__run-fn-form ~run-fn-form
~'__kind ~kind-form
~'__ns ~ns-form
~'__id ~id-form
~'__level ~level-form]
(enc/if-not ~allow?
~run-form*
(let [~@binds-form-base
~@binds-form-more
signal# ~signal-delay-form]
(dispatch-signal!
;; Unconditionally send same wrapped signal to all handlers.
;; Each handler will use wrapper for handler filtering,
;; unwrapping (realizing) only allowed signals.
(WrappedSignal. ~'__kind ~'__ns ~'__id ~'__level signal#))
(if ~'__run-result
( ~'__run-result signal#)
true))))))))))))
(comment
(with-signal (signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)}))
(macroexpand '(signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)}))
(macroexpand '(signal! {:level :info}))
(do
(println "---")
(sigs/with-handler *sig-handlers* "hf1" (fn hf1 [x] (println x)) {}
(signal! {:level :info, :run "run"}))))
;;;; Interop
#?(:clj
(do
(enc/defonce ^:private interop-checks_
"{<source-id> (fn check [])}"
(atom
{:tools-logging (fn [] {:present? present:tools-logging?, :enabled-by-env? enabled:tools-logging?})
:slf4j (fn [] {:present? present:slf4j?, :telemere-provider-present? present:telemere-slf4j?})
:open-telemetry (fn [] {:present? present:otel?, :use-tracer? enabled:otel-tracing?})}))
(defn add-interop-check! [source-id check-fn] (swap! interop-checks_ assoc source-id check-fn))
(defn ^:public check-interop
"Runs Telemere's registered interop checks and returns info useful
for tests/debugging, e.g.:
{:open-telemetry {:present? false}
:tools-logging {:present? false}
:slf4j {:present? true
:sending->telemere? true
:telemere-receiving? true}
...}"
[]
(enc/map-vals (fn [check-fn] (check-fn))
@interop-checks_))
(defn test-interop! [msg test-fn]
(let [msg (str "Interop test: " msg " (" (enc/uuid-str) ")")
signal
(binding [*rt-call-filter* nil] ; Without runtime filters
(with-signal :raw :trap (test-fn msg)))]
(= (force (get signal :msg_)) msg)))))

View file

@ -0,0 +1,411 @@
(ns taoensso.telemere.open-telemetry
"Telemere -> OpenTelemetry handler using `opentelemetry-java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>,
<https://javadoc.io/doc/io.opentelemetry/opentelemetry-api/latest/index.html>
Telemere will attempt to load this ns automatically when possible."
(:require
[clojure.string :as str]
[clojure.set :as set]
[taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.utils :as utils]
[taoensso.telemere.impl :as impl]
[taoensso.telemere :as tel])
(:import
[io.opentelemetry.api.common AttributesBuilder Attributes]
[io.opentelemetry.api.logs LoggerProvider Severity]
[io.opentelemetry.api.trace TracerProvider]))
(comment
(remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview)))
;;;; TODO
;; - API for remote span context and trace state? (Ref. beta19)
;; - API for span links?
;;;; Attributes
(def ^:private ^String attr-name
"Returns cached OpenTelemetry-style name: `:a.b/c-d` -> \"a.b.c_d\", etc.
Ref. <https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/>."
(enc/fmemoize
(fn self
([prefix x] (str (self prefix) "." (self x)))
([ x]
(if-not (enc/named? x)
(str/replace (str/lower-case (str x)) #"[-\s]" "_")
(if-let [ns (namespace x)]
(str/replace (str/lower-case (str ns "." (name x))) "-" "_")
(str/replace (str/lower-case (name x)) "-" "_")))))))
(comment (enc/qb 1e6 (attr-name :a.b/c-d) (attr-name :x.y/z :a.b/c-d))) ; [44.13 63.19]
;; AttributeTypes: String, Long, Double, Boolean, and arrays
(defprotocol ^:private IAttributesBuilder (^:private -put-attr! ^AttributesBuilder [attr-val attr-name attrs-builder]))
(extend-protocol IAttributesBuilder
;; nil (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k "nil")) ; As pr-edn*
nil (-put-attr! [v ^String k ^AttributesBuilder ab] ab ) ; Noop
Boolean (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
String (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
java.util.UUID (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (str v))) ; "d4fc65a0..."
clojure.lang.Named (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (str v))) ; ":foo/bar"
Long (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
Integer (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Short (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Byte (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Double (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
Float (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (double v)))
Number (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (double v)))
clojure.lang.IPersistentCollection
(-put-attr! [v ^String k ^AttributesBuilder ab]
(if (map? v)
(when-let [^String s (truss/catching :common (enc/pr-edn* v))]
(.put ab k s))
(when-some [v1 (if (indexed? v) (nth v 0 nil) (first v))]
(or
(cond
(string? v1) (truss/catching :common (.put ab k ^"[Ljava.lang.String;" (into-array String v)))
(int? v1) (truss/catching :common (.put ab k (long-array v)))
(float? v1) (truss/catching :common (.put ab k (double-array v)))
(boolean? v1) (truss/catching :common (.put ab k (boolean-array v))))
(when-let [^String s (truss/catching :common (enc/pr-edn* v))]
(.put ab k s)))))
ab)
Object
(-put-attr! [v ^String k ^AttributesBuilder ab]
(when-let [^String s (truss/catching :common (enc/pr-edn* v))]
(.put ab k s))))
(defmacro ^:private put-attr! [attrs-builder attr-name attr-val]
`(-put-attr! ~attr-val ~attr-name ~attrs-builder)) ; Fix arg order
(defn- put-attrs!
[^AttributesBuilder attrs-builder attrs]
(cond
(map? attrs) (enc/run-kv! (fn [k v] (put-attr! attrs-builder (attr-name k) v)) attrs) ; Unprefixed
(instance? Attributes attrs) (.putAll attrs-builder ^Attributes attrs) ; Unprefixed
:else
(truss/unexpected-arg! attrs
{:param 'attrs
:context `put-attrs!
:expected #{nil map io.opentelemetry.api.common.Attributes}})))
(defn- merge-attrs!
"If given a map, merges prefixed key/values (~like `into`).
Otherwise just puts single named value."
[attrs-builder name-or-prefix x]
(if (map? x)
(enc/run-kv! (fn [k v] (put-attr! attrs-builder (attr-name name-or-prefix k) v)) x)
(do (put-attr! attrs-builder name-or-prefix x))))
;;;; Handler
(defn- level->severity
^Severity [level]
(case level
:trace Severity/TRACE
:debug Severity/DEBUG
:info Severity/INFO
:warn Severity/WARN
:error Severity/ERROR
:fatal Severity/FATAL
:report Severity/INFO4
Severity/UNDEFINED_SEVERITY_NUMBER))
(defn- level->string
^String [level]
(when level
(case level
:trace "TRACE"
:debug "DEBUG"
:info "INFO"
:warn "WARN"
:error "ERROR"
:fatal "FATAL"
:report "INFO4"
(str level))))
(defn- signal->attrs
"Returns `Attributes` for given signal.
Ref. <https://opentelemetry.io/docs/specs/otel/logs/data-model/>,
<https://opentelemetry.io/docs/specs/semconv/attributes-registry/>."
^Attributes [signal]
(let [ab (Attributes/builder)]
(put-attr! ab "error" (utils/error-signal? signal)) ; Standard
;; (put-attr! ab "host.name" (utils/hostname)) ; Standard
(when-let [{:keys [name ip]} (get signal :host)]
;; Both standard
(put-attr! ab "host.name" name)
(put-attr! ab "host.ip" ip))
(when-let [{:keys [name id]} (get signal :thread)]
;; Both standard
(put-attr! ab "thread.name" name)
(put-attr! ab "thread.id" id))
(when-let [level (get signal :level)]
(put-attr! ab "level" (level->string level)))
(when-let [{:keys [type msg trace data]} (truss/ex-map (get signal :error))]
;; Standard
(put-attr! ab "exception.type" type)
(put-attr! ab "exception.message" msg)
(when trace
(put-attr! ab "exception.stacktrace"
(#'utils/format-clj-stacktrace trace)))
(when data ; Non-standard
(merge-attrs! ab "exception.data" data)))
(let [ns (get signal :ns)]
;; All standard
(put-attr! ab "code.namespace" ns)
(when-let [[line column] (get signal :coords)]
(when line (put-attr! ab "code.line.number" line))
(when column (put-attr! ab "code.column.number" column))))
(let [{:keys [kind id uid]} signal]
(put-attr! ab "kind" kind)
(put-attr! ab "id" id)
(put-attr! ab "uid" uid))
(when-let [run-form (get signal :run-form)]
(let [{:keys [run-val run-nsecs]} signal]
(put-attr! ab "run.form" (if (nil? run-form) "nil" (str run-form)))
(put-attr! ab "run.val_type" (if (nil? run-val) "nil" (.getName (class run-val))))
(put-attr! ab "run.val" run-val)
(put-attr! ab "run.nsecs" run-nsecs)))
(put-attr! ab "sample" (get signal :sample))
(when-let [{:keys [id uid]} (get signal :parent)]
(put-attr! ab "parent.id" id)
(put-attr! ab "parent.uid" uid))
(when-let [{:keys [id uid]} (get signal :root)]
(put-attr! ab "root.id" id)
(put-attr! ab "root.uid" uid))
(when-let [ctx (get signal :ctx)] (merge-attrs! ab "ctx" ctx))
(when-let [data (get signal :data)] (merge-attrs! ab "data" data))
(when-let [attrs (get signal :otel/attrs)] (put-attrs! ab attrs))
(when-let [attrs (get signal :otel/log-attrs)] (put-attrs! ab attrs))
(.build ab)))
(comment
(enc/qb 1e6 ; 808.56
(signal->attrs
{:level :info :data {:ns/kw1 :v1 :ns/kw2 :v2}
:otel/attrs {:longs [1 1 2 3] :strs ["a" "b" "c"]}})))
(let [ak-ns (io.opentelemetry.api.common.AttributeKey/stringKey "ns")
ak-line (io.opentelemetry.api.common.AttributeKey/longKey "line")]
(defn- span-attrs
"Returns `?Attributes`."
[signal]
(let [common-attrs (get signal :otel/attrs)
trace-attrs (get signal :otel/trace-attrs)]
(if (or common-attrs trace-attrs)
(let [ab (Attributes/builder)]
(when-let [ns (get signal :ns)] (.put ab "ns" (str ns)))
(when-let [line (enc/get-in* signal [:coords 0])] (.put ab "line" (long line)))
(when-let [attrs common-attrs] (put-attrs! ab attrs))
(when-let [attrs trace-attrs] (put-attrs! ab attrs))
(.build ab))
;; Common case
(when-let [ns (get signal :ns)]
(if-let [line (enc/get-in* signal [:coords 0])]
(Attributes/of ak-ns ns, ak-line (long line))
(Attributes/of ak-ns ns)))))))
(comment
(enc/qb 1e6 (span-attrs {:ns "ns1" :line 495})) ; 54.31
(span-attrs {:ns "ns1", :otel/attrs {:foo :bar}})
(span-attrs {:ns "ns1", :otel/attrs {:foo {:a :b}}}))
(defn handler:open-telemetry
"Highly experimental, possibly buggy, and subject to change!!
Feedback and bug reports very welcome! Please ping me (Peter) at:
<https://www.taoensso.com/telemere> or
<https://www.taoensso.com/telemere/slack>
Needs `opentelemetry-java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>.
Returns a signal handler that:
- Takes a Telemere signal (map).
- Emits signal data to configured `LogExporter`
- Emits tracing data to configured `SpanExporter`
iff `telemere/otel-tracing?` is true.
Options:
`:logger-provider` - nil or `io.opentelemetry.api.logs.LoggerProvider`,
(see `telemere/otel-default-providers_` for default).
Optional signal keys:
`:otel/attrs` ------- Attributes [1] to add to log records AND tracing spans/events
`:otel/log-attrs` --- Attributes [1] to add to log records ONLY
`:otel/trace-attrs` - Attributes [1] to add to tracing spans/events ONLY
`:otel/span-kind` --- Span kind #{:internal (default) :client :server :consumer :producer}
[1] `io.opentelemetry.api.common.Attributes` or Clojure map with str/kw keys and vals
#{nil boolean keyword string UUID long double string-vec long-vec double-vec boolean-vec}.
Other val types (incl. maps) will be printed as EDN if possible, or skipped otherwise."
;; Notes:
;; - Multi-threaded handlers may see signals ~out of order
;; - Sampling means that root/parent/child signals might not be handled
;; - `:otel/attrs`, `:otel/context` currently undocumented
([] (handler:open-telemetry nil))
([{:keys [emit-tracing? logger-provider]
:or {emit-tracing? true}}]
(let [?logger-provider
(if (not= logger-provider :default)
logger-provider
(:logger-provider (force tel/otel-default-providers_)))
;; Mechanism to end spans 3-6 secs *after* signal handling. The delay
;; helps support out-of-order signals due to >1 handler threads, etc.
span-buffer1_ (enc/latom #{}) ; #{[<Span> <end-inst>]}
span-buffer2_ (enc/latom #{})
timer_
(delay
(let [t3s (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimer3s" (boolean :daemon))]
(.schedule t3s
(proxy [java.util.TimerTask] []
(run []
;; span2->end!
(when-let [drained (enc/reset-in! span-buffer2_ #{})]
(doseq [[span end-inst] drained]
(.end
^io.opentelemetry.api.trace.Span span
^java.time.Instant end-inst)))
;; span1->span2
(when-let [drained (enc/reset-in! span-buffer1_ #{})]
(when-not (empty? drained)
(span-buffer2_ (fn [old] (set/union old drained)))))))
3000 3000)
t3s))
stop-tracing!
(fn stop-tracing! []
(when (realized? timer_)
(loop [] (when-not (empty? (span-buffer1_)) (recur))) ; Block to drain `span1`
(loop [] (when-not (empty? (span-buffer2_)) (recur))) ; Block to drain `span2`
(.cancel ^java.util.Timer @timer_)))]
(fn a-handler:open-telemetry
([ ] (stop-tracing!))
([signal]
(let [?tracing-context
(when emit-tracing?
(when-let [context (enc/get* signal :otel/context :_otel-context nil)]
(let [span (io.opentelemetry.api.trace.Span/fromContext context)]
(when (.isRecording span)
(enc/if-not [end-inst (get signal :end-inst)]
;; No end-inst => no run-form => add `Event` to span (parent)
(let [{:keys [id ^java.time.Instant inst]} signal]
(if-let [^Attributes attrs (span-attrs signal)]
(.addEvent span (impl/otel-name id) attrs inst)
(.addEvent span (impl/otel-name id) inst)))
;; Real span
(do
(if (utils/error-signal? signal)
(.setStatus span io.opentelemetry.api.trace.StatusCode/ERROR)
(.setStatus span io.opentelemetry.api.trace.StatusCode/OK))
(when-let [^Attributes attrs (span-attrs signal)]
(.setAllAttributes span attrs))
;; Error stuff
(when-let [error (get signal :error)]
(when (instance? Throwable error)
(if-let [attrs
(when-let [ex-data (ex-data error)]
(when-not (empty? ex-data)
(let [sb (Attributes/builder)]
(enc/run-kv! (fn [k v] (put-attr! sb (attr-name k) v)) ex-data)
(.build sb))))]
(.recordException span error attrs)
(.recordException span error))))
;; (.end span end-inst) ; Emit to `SpanExporter` now
;; Emit to `SpanExporter` after delay:
(span-buffer1_ (fn [old] (conj old [span end-inst])))
(.deref timer_) ; Ensure timer is running
))
context))))]
(when-let [^io.opentelemetry.api.logs.LoggerProvider logger-provider ?logger-provider]
(let [{:keys [ns inst level msg_]} signal
logger (.get logger-provider (or ns "default"))
lrb (.logRecordBuilder logger)]
(.setTimestamp lrb inst)
(.setSeverity lrb (level->severity level))
(.setAllAttributes lrb (signal->attrs signal))
(when-let [^io.opentelemetry.context.Context tracing-context ?tracing-context]
(.setContext lrb tracing-context)) ; Incl. traceId, spanId, etc.
(when-let [^String body
(or
(force msg_)
(when-let [error (get signal :error)]
(when (instance? Throwable error)
(str (truss/ex-type error) ": " (ex-message error)))))]
(.setBody lrb body))
;; Emit to `LogRecordExporter`
(.emit lrb)))))))))
(enc/deprecated
(def ^:no-doc ^:deprecated handler:open-telemetry-logger
"Prefer `handler:open-telemetry`."
handler:open-telemetry))
(comment
(do
(require '[taoensso.telemere :as tel])
(def h1 (handler:open-telemetry))
(let [{[s1 s2] :signals} (tel/with-signals (tel/trace! ::id1 (tel/trace! ::id2 "form2")))]
(def s1 s1)
(def s2 s2)))
(h1 s1))
(defn check-interop
"Returns interop debug info map."
[]
{:present? true
:use-tracer? impl/enabled:otel-tracing?
:viable-tracer? (boolean (impl/viable-tracer (force tel/*otel-tracer*)))})
(impl/add-interop-check! :open-telemetry check-interop)
(impl/on-init
(when impl/enabled:otel-tracing?
;; (tel/add-handler! :default/open-telemetry (handler:open-telemetry))
(impl/signal!
{:kind :event
:level :debug ; < :info since runs on init
:id :taoensso.telemere/open-telemetry-tracing!
:msg "Enabling interop: OpenTelemetry tracing"})))

View file

@ -1,57 +1,28 @@
(ns taoensso.telemere.postal (ns taoensso.telemere.postal
"Email handler using `postal`, "Telemere -> email handler using `postal`,
Ref. <https://github.com/drewr/postal>." Ref. <https://github.com/drewr/postal>."
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.encore.signals :as sigs]
[taoensso.telemere.utils :as utils] [taoensso.telemere.utils :as utils]
[postal.core :as postal])) [postal.core :as postal]))
(comment (comment
(require '[taoensso.telemere :as tel]) (require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.postal) (remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview))) (:api (enc/interns-overview)))
(defn signal-subject-fn
"Experimental, subject to change.
Returns a (fn format [signal]) that:
- Takes a Telemere signal (map).
- Returns an email subject string like:
\"INFO EVENT :taoensso.telemere.postal/ev-id1 - msg\""
([] (signal-subject-fn nil))
([{:keys [max-len subject-signal-key]
:or
{max-len 128
subject-signal-key :postal/subject}}]
(fn signal-subject [signal]
(or
(get signal subject-signal-key) ; Custom subject
;; Simplified `utils/signal-preamble-fn`
(let [{:keys [level kind #_ns id msg_]} signal
sb (enc/str-builder)
s+spc (enc/sb-appender sb " ")]
(when level (s+spc (utils/format-level level)))
(when kind (s+spc (utils/upper-qn kind)))
(when id (s+spc (utils/format-id nil id)))
(when-let [msg (force msg_)] (s+spc "- " msg))
(enc/get-substr-by-len (str sb) 0 max-len))))))
(comment ((signal-subject-fn) (tel/with-signal (tel/event! ::ev-id1 #_{:postal/subject "My subject"}))))
(def default-dispatch-opts (def default-dispatch-opts
{:min-level :info {:min-level :info
:rate-limit :limit
[[5 (enc/msecs :mins 1)] [[5 (enc/msecs :mins 1)]
[10 (enc/msecs :mins 15)] [10 (enc/msecs :mins 15)]
[15 (enc/msecs :hours 1)] [15 (enc/msecs :hours 1)]
[30 (enc/msecs :hours 6)] [30 (enc/msecs :hours 6)]]})
]})
(defn handler:postal (defn handler:postal
"Experimental, subject to change. "Alpha, subject to change.
Needs `postal`, Ref. <https://github.com/drewr/postal>. Needs `postal`, Ref. <https://github.com/drewr/postal>.
@ -63,7 +34,7 @@
Default handler dispatch options (override when calling `add-handler!`): Default handler dispatch options (override when calling `add-handler!`):
`:min-level` - `:info` `:min-level` - `:info`
`:rate-limit` - `:limit` -
[[5 (enc/msecs :mins 1)] ; Max 5 emails in 1 min [[5 (enc/msecs :mins 1)] ; Max 5 emails in 1 min
[10 (enc/msecs :mins 15)] ; Max 10 emails in 15 mins [10 (enc/msecs :mins 15)] ; Max 10 emails in 15 mins
[15 (enc/msecs :hours 1)] ; Max 15 emails in 1 hour [15 (enc/msecs :hours 1)] ; Max 15 emails in 1 hour
@ -87,7 +58,9 @@
:cc \"engineering@example.com\" :cc \"engineering@example.com\"
:X-MyHeader \"A custom header\"} :X-MyHeader \"A custom header\"}
`:subject-fn` - (fn [signal]) => email subject string `:subject-fn` ------ (fn [signal]) => email subject string
`:subject-max-len` - Truncate subjects beyond this length (default 90)
`:body-fn` - (fn [signal]) => email body content string, `:body-fn` - (fn [signal]) => email body content string,
see `format-signal-fn` or `pr-signal-fn` see `format-signal-fn` or `pr-signal-fn`
@ -97,15 +70,22 @@
Use appropriate handler dispatch options for async handling and rate limiting, etc." Use appropriate handler dispatch options for async handling and rate limiting, etc."
;; ([] (handler:postal nil)) ;; ([] (handler:postal nil))
([{:keys [conn-opts msg-opts, subject-fn body-fn] ([{:keys [conn-opts msg-opts, subject-fn subject-max-len body-fn]
:or :or
{subject-fn (signal-subject-fn) {body-fn (utils/format-signal-fn)
body-fn (utils/format-signal-fn)}}] subject-fn (utils/signal-preamble-fn {:format-inst-fn nil})
subject-max-len 128}}]
(when-not (map? conn-opts) (throw (ex-info "Expected `:conn-opts` map" (enc/typed-val conn-opts)))) (when-not (map? conn-opts) (truss/ex-info! "Expected `:conn-opts` map" (truss/typed-val conn-opts)))
(when-not (map? msg-opts) (throw (ex-info "Expected `:msg-opts` map" (enc/typed-val msg-opts)))) (when-not (map? msg-opts) (truss/ex-info! "Expected `:msg-opts` map" (truss/typed-val msg-opts)))
(let [handler-fn (let [subject-fn
(if-let [n subject-max-len]
(comp
(fn [s] (when s (enc/substr (str s) 0 n)))
subject-fn))
handler-fn
(fn a-handler:postal (fn a-handler:postal
([ ]) ; Stop => noop ([ ]) ; Stop => noop
([signal] ([signal]
@ -128,7 +108,7 @@
success? (= (get result :code) 0)] success? (= (get result :code) 0)]
(when-not success? (when-not success?
(throw (ex-info "Failed to send email" result ex)))))))] (truss/ex-info! "Failed to send email" result ex))))))]
(with-meta handler-fn (with-meta handler-fn
{:dispatch-opts default-dispatch-opts})))) {:dispatch-opts default-dispatch-opts}))))

View file

@ -1,28 +1,28 @@
(ns taoensso.telemere.slack (ns taoensso.telemere.slack
"Slack handler using `clj-slack`, "Telemere -> Slack handler using `clj-slack`,
Ref. <https://github.com/julienXX/clj-slack>" Ref. <https://github.com/julienXX/clj-slack>"
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.utils :as utils] [taoensso.telemere.utils :as utils]
[clj-slack.core :as slack] [clj-slack.core :as slack]
[clj-slack.chat :as slack.chat])) [clj-slack.chat :as slack.chat]))
(comment (comment
(require '[taoensso.telemere :as tel]) (require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.slack) (remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview))) (:api (enc/interns-overview)))
(def default-dispatch-opts (def default-dispatch-opts
{:min-level :info {:min-level :info
:rate-limit :limit
[[5 (enc/msecs :mins 1)] [[5 (enc/msecs :mins 1)]
[10 (enc/msecs :mins 15)] [10 (enc/msecs :mins 15)]
[15 (enc/msecs :hours 1)] [15 (enc/msecs :hours 1)]
[30 (enc/msecs :hours 6)] [30 (enc/msecs :hours 6)]]})
]})
(defn handler:slack (defn handler:slack
"Experimental, subject to change. "Alpha, subject to change.
Needs `clj-slack`, Ref. <https://github.com/julienXX/clj-slack>. Needs `clj-slack`, Ref. <https://github.com/julienXX/clj-slack>.
@ -34,7 +34,7 @@
Default handler dispatch options (override when calling `add-handler!`): Default handler dispatch options (override when calling `add-handler!`):
`:min-level` - `:info` `:min-level` - `:info`
`:rate-limit` - `:limit` -
[[5 (enc/msecs :mins 1)] ; Max 5 posts in 1 min [[5 (enc/msecs :mins 1)] ; Max 5 posts in 1 min
[10 (enc/msecs :mins 15)] ; Max 10 posts in 15 mins [10 (enc/msecs :mins 15)] ; Max 10 posts in 15 mins
[15 (enc/msecs :hours 1)] ; Max 15 posts in 1 hour [15 (enc/msecs :hours 1)] ; Max 15 posts in 1 hour
@ -68,8 +68,8 @@
{:keys [channel-id]} post-opts {:keys [channel-id]} post-opts
post-opts (dissoc post-opts :channel-id) post-opts (dissoc post-opts :channel-id)
_ (when-not (string? token) (throw (ex-info "Expected `:conn-opts/token` string" (enc/typed-val token)))) _ (when-not (string? token) (truss/ex-info! "Expected `:conn-opts/token` string" (truss/typed-val token)))
_ (when-not (string? channel-id) (throw (ex-info "Expected `:post-opts/channel-id` string" (enc/typed-val channel-id)))) _ (when-not (string? channel-id) (truss/ex-info! "Expected `:post-opts/channel-id` string" (truss/typed-val channel-id)))
handler-fn handler-fn
(fn a-handler:slack (fn a-handler:slack

View file

@ -1,7 +1,8 @@
(ns taoensso.telemere.sockets (ns taoensso.telemere.sockets
"Basic TCP/UDP socket handlers." "Telemere -> TCP/UDP socket handlers."
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.utils :as utils]) [taoensso.telemere.utils :as utils])
(:import (:import
@ -11,7 +12,7 @@
(comment (comment
(require '[taoensso.telemere :as tel]) (require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.sockets) (remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview))) (:api (enc/interns-overview)))
(defn handler:tcp-socket (defn handler:tcp-socket
@ -24,11 +25,11 @@
Can output signals as human or machine-readable (edn, JSON) strings. Can output signals as human or machine-readable (edn, JSON) strings.
Options: Options:
`:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn` `:output-fn` --- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
`:socket-opts` - {:keys [host port ssl? connect-timeout-msecs]} `:socket-opts` - {:keys [host port ssl? connect-timeout-msecs]}
`:host` - Destination TCP socket hostname string `:host` ------ Destination TCP socket hostname string
`:port` - Destination TCP socket port int `:port` ------ Destination TCP socket port int
`:ssl?` - Use SSL/TLS (default false) `:ssl?` ------ Use SSL/TLS (default false)
`:connect-timeout-msecs` - Connection timeout (default 3000 msecs) `:connect-timeout-msecs` - Connection timeout (default 3000 msecs)
Limitations: Limitations:
@ -48,7 +49,7 @@
(sw output))))))) (sw output)))))))
(defn handler:udp-socket (defn handler:udp-socket
"Highly experimental, subject to change. "Highly experimental, subject to change!
Feedback very welcome! Feedback very welcome!
Returns a signal handler that: Returns a signal handler that:
@ -58,10 +59,10 @@
Can output signals as human or machine-readable (edn, JSON) strings. Can output signals as human or machine-readable (edn, JSON) strings.
Options: Options:
`:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn` `:output-fn` ---------- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
`:socket-opts` - {:keys [host port max-packet-bytes]} `:socket-opts` -------- {:keys [host port max-packet-bytes]}
`:host` - Destination UDP socket hostname string `:host` ------------- Destination UDP socket hostname string
`:port` - Destination UDP socket port int `:port` ------------- Destination UDP socket port int
`:max-packet-bytes` - Max packet size (in bytes) before truncating output (default 512) `:max-packet-bytes` - Max packet size (in bytes) before truncating output (default 512)
`:truncation-warning-fn` `:truncation-warning-fn`
@ -90,8 +91,8 @@
socket (DatagramSocket.) ; No need to change socket once created socket (DatagramSocket.) ; No need to change socket once created
lock (Object.)] lock (Object.)]
(when-not (string? host) (throw (ex-info "Expected `:host` string" (enc/typed-val host)))) (when-not (string? host) (truss/ex-info! "Expected `:host` string" (truss/typed-val host)))
(when-not (int? port) (throw (ex-info "Expected `:port` int" (enc/typed-val port)))) (when-not (int? port) (truss/ex-info! "Expected `:port` int" (truss/typed-val port)))
(.connect socket (InetSocketAddress. (str host) (int port))) (.connect socket (InetSocketAddress. (str host) (int port)))

View file

@ -1,8 +1,8 @@
(ns taoensso.telemere.streams (ns taoensso.telemere.streams
"Intake support for standard stream/s -> Telemere." "Standard streams -> Telemere interop."
(:refer-clojure :exclude [binding])
(:require (:require
[taoensso.encore :as enc :refer [binding have have?]] [taoensso.encore :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.impl :as impl])) [taoensso.telemere.impl :as impl]))
(enc/defonce ^:private orig-*out* "Original `*out*` on ns load" *out*) (enc/defonce ^:private orig-*out* "Original `*out*` on ns load" *out*)
@ -35,8 +35,7 @@
*err* (or prev-*err* orig-*err*)] *err* (or prev-*err* orig-*err*)]
(impl/signal! (impl/signal!
{:location nil {:ns nil
:ns nil
:kind kind :kind kind
:level level :level level
:id id :id id
@ -96,7 +95,7 @@
{:kind :event {:kind :event
:level :info :level :info
:id :taoensso.telemere/streams->telemere! :id :taoensso.telemere/streams->telemere!
:msg "Disabling intake: standard stream/s -> Telemere" :msg "Disabling interop: standard stream/s -> Telemere"
:data {:system/out? (boolean orig-out) :data {:system/out? (boolean orig-out)
:system/err? (boolean orig-err)}}) :system/err? (boolean orig-err)}})
@ -133,7 +132,7 @@
{:kind :event {:kind :event
:level :info :level :info
:id :taoensso.telemere/streams->telemere! :id :taoensso.telemere/streams->telemere!
:msg "Enabling intake: standard stream/s -> Telemere" :msg "Enabling interop: standard stream/s -> Telemere"
:data {:system/out? (boolean out) :data {:system/out? (boolean out)
:system/err? (boolean err)}}) :system/err? (boolean err)}})
@ -150,19 +149,19 @@
;;;; ;;;;
(defn check-out-intake (defn check-out-interop
"Returns {:keys [sending->telemere? telemere-receiving?]}." "Returns interop debug info map."
[] []
(let [sending? (boolean @orig-out_) (let [sending? (boolean @orig-out_)
receiving? (and sending? (impl/test-intake! "`System/out` -> Telemere" #(.println System/out %)))] receiving? (and sending? (impl/test-interop! "`System/out` -> Telemere" #(.println System/out %)))]
{:sending->telemere? sending?, :telemere-receiving? receiving?})) {:sending->telemere? sending?, :telemere-receiving? receiving?}))
(defn check-err-intake (defn check-err-interop
"Returns {:keys [sending->telemere? telemere-receiving?]}." "Returns interop debug info map."
[] []
(let [sending? (boolean @orig-err_) (let [sending? (boolean @orig-err_)
receiving? (and sending? (impl/test-intake! "`System/err` -> Telemere" #(.println System/err %)))] receiving? (and sending? (impl/test-interop! "`System/err` -> Telemere" #(.println System/err %)))]
{:sending->telemere? sending?, :telemere-receiving? receiving?})) {:sending->telemere? sending?, :telemere-receiving? receiving?}))
(impl/add-intake-check! :system/out check-out-intake) (impl/add-interop-check! :system/out check-out-interop)
(impl/add-intake-check! :system/err check-err-intake) (impl/add-interop-check! :system/err check-err-interop)

View file

@ -0,0 +1,212 @@
(ns taoensso.telemere.timbre
"Main Timbre macros, reimplemented on top of Telemere.
Intended to help ease migration from Timbre to Telemere."
(:require
[clojure.string :as str]
[taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.impl :as impl]
[taoensso.telemere :as tel]))
(comment
(remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview)))
(let [arg-str
(fn [x]
(enc/cond
(nil? x) "nil"
(record? x) (pr-str x)
:else x))]
(defn ^:no-doc parse-vargs
"Private, don't use. Adapted from Timbre."
[format-msg? vargs]
(let [[v0] vargs]
(if (truss/error? v0)
(let [error v0
vargs (enc/vrest vargs)
pattern (if format-msg? (let [[v0] vargs] v0) nil)
vargs (if format-msg? (enc/vrest vargs) vargs)
msg
(delay
(if format-msg?
(enc/format* pattern vargs)
(enc/str-join " " (map arg-str) vargs)))]
[error msg vargs])
(let [md (if (and (map? v0) (get (meta v0) :meta)) v0 nil)
error (get md :err)
md (dissoc md :err)
vargs (if md (enc/vrest vargs) vargs)
pattern (if format-msg? (let [[v0] vargs] v0) nil)
vargs (if format-msg? (enc/vrest vargs) vargs)
msg
(delay
(if format-msg?
(enc/format* pattern vargs)
(enc/str-join " " (map arg-str) vargs)))]
[error msg (when-not (empty? vargs) vargs)])))))
(comment
(parse-vargs true [ "hello %s" "stu"])
(parse-vargs true [(Exception. "Ex1") "hello %s" "stu"]))
(def ^:no-doc ^:const shim-id :taoensso.telemere/timbre)
#?(:clj
(defmacro ^:no-doc log!
"Private, don't use."
[level format-msg? vargs]
(truss/keep-callsite
`(when (impl/signal-allowed? {:kind :log, :level ~level, :id shim-id})
(let [[error# msg# vargs#] (parse-vargs ~format-msg? ~vargs)]
(tel/log!
{:allow? true
:level ~level
:id shim-id
:error error#
:timbre/vargs vargs#}
msg#)
nil)))))
(comment
(macroexpand '(trace "foo"))
(tel/with-signal (trace "foo"))
(tel/with-signal (infof "Hello %s" "world")))
#?(:clj
(do
(defmacro log "Prefer `telemere/log!`, etc." [level & args] (truss/keep-callsite `(log! ~level false [~@args])))
(defmacro trace "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :trace false [~@args])))
(defmacro debug "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :debug false [~@args])))
(defmacro info "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :info false [~@args])))
(defmacro warn "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :warn false [~@args])))
(defmacro error "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :error false [~@args])))
(defmacro fatal "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :fatal false [~@args])))
(defmacro report "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :report false [~@args])))
(defmacro logf "Prefer `telemere/log!`, etc." [level & args] (truss/keep-callsite `(log! ~level true [~@args])))
(defmacro tracef "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :trace true [~@args])))
(defmacro debugf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :debug true [~@args])))
(defmacro infof "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :info true [~@args])))
(defmacro warnf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :warn true [~@args])))
(defmacro errorf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :error true [~@args])))
(defmacro fatalf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :fatal true [~@args])))
(defmacro reportf "Prefer `telemere/log!`, etc." [& args] (truss/keep-callsite `(log! :report true [~@args])))))
#?(:clj
(defmacro spy
"Prefer `telemere/spy!`.
Note that for extra flexibility and improved interop with Open Telemetry,
this shim intentionally handles errors (forms that throw) slightly differently
to Timbre's original `spy`:
When the given `form` throws, this shim may create an ADDITIONAL signal of
`:error` kind and level. The behaviour is equivalent to:
(telemere/spy! level ; Creates 0/1 `:spy` signals with given/default (`:debug`) level
(telemere/catch->error! form)) ; Creates 0/1 `:error` signals with `:error` level
The additional signal helps to separate the success and error cases for
individual filtering and/or handling."
([ form] (truss/keep-callsite `(spy :debug nil ~form)))
([level form] (truss/keep-callsite `(spy ~level nil ~form)))
([level form-name form]
(let [ns (str *ns*)
coords (truss/callsite-coords &form)
msg
(if form-name
`(fn [_form# value# error# nsecs#] (impl/default-trace-msg ~form-name value# error# nsecs#))
`(fn [_form# value# error# nsecs#] (impl/default-trace-msg '~form value# error# nsecs#)))]
`(tel/spy!
{:ns ~ns
:coords ~coords
:id shim-id
:level ~level
:msg ~msg}
(tel/catch->error!
{:ns ~ns
:coords ~coords
:id shim-id}
~form))))))
(comment
(:level (tel/with-signal (spy (/ 1 0))))
(select-keys (tel/with-signal (spy :info #_"my-form-name" (+ 1 2))) [:level :msg_])
(select-keys (tel/with-signal (spy :info #_"my-form-name" (throw (Exception. "Ex")))) [:level :msg_]))
#?(:clj (defmacro log-errors "Prefer `telemere/catch->error!`." [& body] (truss/keep-callsite `(tel/catch->error! {:id shim-id, :catch-val nil} (do ~@body)))))
#?(:clj (defmacro log-and-rethrow-errors "Prefer `telemere/catch->error!`." [& body] (truss/keep-callsite `(tel/catch->error! {:id shim-id} (do ~@body)))))
#?(:clj (defmacro logged-future "Prefer `telemere/catch->error!`." [& body] (truss/keep-callsite `(future (tel/catch->error! {:id shim-id} (do ~@body))))))
#?(:clj
(defmacro refer-timbre
"(require
'[taoensso.telemere.timbre :as timbre :refer
[log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy]])"
[]
`(require
'~'[taoensso.telemere.timbre :as timbre :refer
[log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy]])))
;;;;
(defn set-min-level! "Prefer `telemere/set-min-level!`." [min-level] (tel/set-min-level! min-level))
#?(:clj
(defmacro with-min-level
"Prefer `telemere/with-min-level`."
[min-level & body]
`(tel/with-min-level ~min-level (do ~@body))))
#?(:clj
(defmacro set-ns-min-level!
"Prefer `telemere/set-min-level!`."
([ ?min-level] `(set-ns-min-level! ~(str *ns*) ~?min-level))
([ns ?min-level] `(tel/set-min-level! nil ~(str ns) ~?min-level))))
#?(:clj (defmacro with-context "Prefer `telemere/with-ctx`." [context & body] `(tel/with-ctx ~context (do ~@body))))
#?(:clj (defmacro with-context+ "Prefer `telemere/with-ctx+`." [context & body] `(tel/with-ctx+ ~context (do ~@body))))
(defn shutdown-appenders!
"Prefer `telemere/stop-handlers!`."
[] (tel/stop-handlers!))
(defn timbre->telemere-appender
"Returns a simple Timbre appender that'll redirect logs to Telemere."
[]
{:enabled? true
:min-level nil
:fn
(fn [data]
(let [{:keys [instant level context ?err msg-type vargs
?ns-str ?file ?line ?column]} data
format-msg? (enc/identical-kw? msg-type :f)
[_error msg vargs] (parse-vargs format-msg? vargs)]
(taoensso.telemere/signal!
{:kind :timbre
:level level
:inst (taoensso.encore/as-?inst instant)
:ctx+ context
:ns ?ns-str
:coords (when ?line [?line ?column])
:file ?file ; Non-standard, goes to kvs
:error ?err
:msg (when msg-type msg)
:timbre/vargs vargs})))})

View file

@ -1,5 +1,5 @@
(ns taoensso.telemere.tools-logging (ns taoensso.telemere.tools-logging
"Intake support for `tools.logging` -> Telemere. "tools.logging -> Telemere interop.
Telemere will attempt to load this ns automatically when possible. Telemere will attempt to load this ns automatically when possible.
Naming conventions: Naming conventions:
@ -8,19 +8,20 @@
`clojure.tools.logging` - For env config to match library's conventions." `clojure.tools.logging` - For env config to match library's conventions."
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.impl :as impl] [taoensso.telemere.impl :as impl]
[clojure.tools.logging :as ctl])) [clojure.tools.logging :as ctl]))
(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body))) (defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))
(deftype TelemereLogger [logger-name] (deftype TelemereLogger [logger-name]
;; `logger-name` is typically ns string
clojure.tools.logging.impl/Logger clojure.tools.logging.impl/Logger
(enabled? [_ level] (enabled? [_ level]
(when-debug (println [:tools-logging/enabled? level logger-name])) (when-debug (println [:tools-logging/enabled? level logger-name]))
(impl/signal-allowed? (impl/signal-allowed?
{:location {:ns logger-name} ; Typically *ns* string {:ns logger-name
:kind :tools-logging :kind :tools-logging
:level level})) :level level}))
@ -28,7 +29,7 @@
(when-debug (println [:tools-logging/write! level logger-name])) (when-debug (println [:tools-logging/write! level logger-name]))
(impl/signal! (impl/signal!
{:allow? true ; Pre-filtered by `enabled?` call {:allow? true ; Pre-filtered by `enabled?` call
:location {:ns logger-name} ; Typically *ns* string :ns logger-name
:kind :tools-logging :kind :tools-logging
:level level :level level
:error throwable :error throwable
@ -41,25 +42,25 @@
(get-logger [_ logger-name] (TelemereLogger. (str logger-name)))) (get-logger [_ logger-name] (TelemereLogger. (str logger-name))))
(defn tools-logging->telemere! (defn tools-logging->telemere!
"Configures `tools.logging` to use Telemere as its logging "Configures tools.logging to use Telemere as its logging
implementation (backend). implementation (backend).
Called automatically if one of the following is \"true\": Called automatically if one of the following is \"true\":
JVM property: `clojure.tools.logging.to-telemere` 1. JVM property: `clojure.tools.logging.to-telemere`
Env variable: `CLOJURE_TOOLS_LOGGING_TO_TELEMERE` 2. Env variable: `CLOJURE_TOOLS_LOGGING_TO_TELEMERE`
Classpath resource: `clojure.tools.logging.to-telemere`" 3. Classpath resource: `clojure.tools.logging.to-telemere`"
[] []
(impl/signal! (impl/signal!
{:kind :event {:kind :event
:level :debug ; < :info since runs on init :level :debug ; < :info since runs on init
:id :taoensso.telemere/tools-logging->telemere! :id :taoensso.telemere/tools-logging->telemere!
:msg "Enabling intake: `tools.logging` -> Telemere"}) :msg "Enabling interop: tools.logging -> Telemere"})
(alter-var-root #'clojure.tools.logging/*logger-factory* (alter-var-root #'clojure.tools.logging/*logger-factory*
(fn [_] (TelemereLoggerFactory.)))) (fn [_] (TelemereLoggerFactory.))))
(defn tools-logging->telemere? (defn tools-logging->telemere?
"Returns true iff `tools.logging` is configured to use Telemere "Returns true iff tools.logging is configured to use Telemere
as its logging implementation (backend)." as its logging implementation (backend)."
[] []
(when-let [lf clojure.tools.logging/*logger-factory*] (when-let [lf clojure.tools.logging/*logger-factory*]
@ -67,21 +68,22 @@
;;;; ;;;;
(defn check-intake (defn check-interop
"Returns {:keys [present? sending->telemere? telemere-receiving?]}." "Returns interop debug info map."
[] []
(let [sending? (tools-logging->telemere?) (let [sending? (tools-logging->telemere?)
receiving? receiving?
(and sending? (and sending?
(impl/test-intake! "`tools.logging` -> Telemere" (impl/test-interop! "tools.logging -> Telemere"
#(clojure.tools.logging/info %)))] #(clojure.tools.logging/info %)))]
{:present? true {:present? true
:enabled-by-env? impl/enabled:tools-logging?
:sending->telemere? sending? :sending->telemere? sending?
:telemere-receiving? receiving?})) :telemere-receiving? receiving?}))
(impl/add-intake-check! :tools-logging check-intake) (impl/add-interop-check! :tools-logging check-interop)
(impl/on-init (impl/on-init
(when (enc/get-env {:as :bool} :clojure.tools.logging/to-telemere) (when impl/enabled:tools-logging?
(tools-logging->telemere!))) (tools-logging->telemere!)))

View file

@ -1,60 +1,27 @@
(ns taoensso.telemere.utils (ns taoensso.telemere.utils
"Misc utils useful for Telemere handlers, middleware, etc." "Misc utils useful for Telemere handlers, transforms, etc."
(:refer-clojure :exclude [newline]) (:refer-clojure :exclude [newline])
(:require (:require
[clojure.string :as str] [clojure.string :as str]
#?(:clj [clojure.java.io :as jio]) #?(:clj [clojure.java.io :as jio])
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.encore.signals :as sigs]
[taoensso.telemere.impl :as impl])) [taoensso.telemere.impl :as impl]))
(comment (comment
(require '[taoensso.telemere :as tel]) (require '[taoensso.telemere :as tel])
(remove-ns 'taoensso.telemere.utils) (remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview))) (:api (enc/interns-overview)))
;;;; Private ;;;;
(enc/def* ^:no-doc upper-qn (enc/defaliases #_sigs/upper-qn sigs/format-level sigs/format-id)
"Private, don't use.
`:foo/bar` -> \"FOO/BAR\", etc."
{:tag #?(:clj 'String :cljs 'string)}
(enc/fmemoize (fn [x] (str/upper-case (enc/as-qname x)))))
(comment (upper-qn :foo/bar))
(enc/def* ^:no-doc format-level
"Private, don't use.
`:info` -> \"INFO\",
`5` -> \"LEVEL:5\", etc."
{:tag #?(:clj 'String :cljs 'string)}
(enc/fmemoize
(fn [x]
(if (keyword? x)
(upper-qn x)
(str "LEVEL:" x)))))
(comment (format-level :info))
(enc/def* ^:no-doc format-id
"Private, don't use.
`:foo.bar/baz` -> \"::baz\", etc."
{:tag #?(:clj 'String :cljs 'string)}
(enc/fmemoize
(fn [ns x]
(if (keyword? x)
(if (= (namespace x) ns)
(str "::" (name x))
(str x))
(str x)))))
(comment
(format-id (str *ns*) ::id1)
(format-id nil ::id1))
;;;; Unique IDs (UIDs) ;;;; Unique IDs (UIDs)
(defn nano-uid-fn (defn nano-uid-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn nano-uid [root?]) that returns a random nano-style uid string like: Returns a (fn nano-uid [root?]) that returns a random nano-style uid string like:
\"r76-B8LoIPs5lBG1_Uhdy\" - 126 bit (21 char) root uid \"r76-B8LoIPs5lBG1_Uhdy\" - 126 bit (21 char) root uid
\"tMEYoZH0K-\" - 60 bit (10 char) non-root (child) uid" \"tMEYoZH0K-\" - 60 bit (10 char) non-root (child) uid"
@ -85,7 +52,7 @@
(comment ((nano-uid-fn) true)) (comment ((nano-uid-fn) true))
(defn hex-uid-fn (defn hex-uid-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn hex-uid [root?]) that returns a random hex-style uid string like: Returns a (fn hex-uid [root?]) that returns a random hex-style uid string like:
\"05039666eb9dc3206475f44ab9f3d843\" - 128 bit (32 char) root uid \"05039666eb9dc3206475f44ab9f3d843\" - 128 bit (32 char) root uid
\"721fcef639a51513\" - 64 bit (16 char) non-root (child) uid" \"721fcef639a51513\" - 64 bit (16 char) non-root (child) uid"
@ -125,10 +92,45 @@
(comment ((hex-uid-fn) true)) (comment ((hex-uid-fn) true))
(comment (comment
;; [170.47 180.18 53.87 60.68] ;; [168.74 180.83 65.28 47.3]
(let [nano-uid-fn (nano-uid-fn), hex-uid-fn (hex-uid-fn)] (let [nano-uid (nano-uid-fn), hex-uid (hex-uid-fn)]
(enc/qb 1e6 (enc/uuid) (enc/uuid-str) (nano-uid true) (hex-uid true)))) (enc/qb 1e6 (enc/uuid) (enc/uuid-str) (nano-uid true) (hex-uid true))))
(defn ^:no-doc parse-uid-fn
"Private, don't use.
Returns (fn uid [root?]) for given uid kind."
[kind]
(case kind
:uuid (fn [_root?] (enc/uuid))
:uuid-str (fn [_root?] (enc/uuid-str))
:default (nano-uid-fn {:secure? false})
:nano/insecure (nano-uid-fn {:secure? false})
:nano/secure (nano-uid-fn {:secure? true})
:hex/insecure (hex-uid-fn {:secure? false})
:hex/secure (hex-uid-fn {:secure? true})
(or
(when (vector? kind)
(let [[kind root-len child-len] kind]
(case kind
:nano/insecure (nano-uid-fn {:secure? false, :root-len root-len, :child-len child-len})
:nano/secure (nano-uid-fn {:secure? true, :root-len root-len, :child-len child-len})
:hex/insecure (hex-uid-fn {:secure? false, :root-len root-len, :child-len child-len})
:hex/secure (hex-uid-fn {:secure? true, :root-len root-len, :child-len child-len})
nil)))
(truss/unexpected-arg! kind
{:param 'kind
:context `uid-fn
:expected
'#{:uuid :uuid-str :default,
:nano/secure [:nano/secure <root-len> <child-len>]
:nano/insecure [:nano/insecure <root-len> <child-len>]
:hex/secure [:hex/secure <root-len> <child-len>]
:hex/insecure [:hex/insecure <root-len> <child-len>]}}))))
(comment ((parse-uid-fn [:hex/insecure 32 16]) true))
;;;; Misc ;;;; Misc
(enc/defaliases (enc/defaliases
@ -140,14 +142,14 @@
#?(:cljs #?(:cljs
(defn js-console-logger (defn js-console-logger
"Returns JavaScript console logger to match given signal level: "Returns JavaScript console logger to match given signal level:
`:trace` -> `js/console.trace`, `:debug` -> `js/console.debug`,
`:error` -> `js/console.error`, etc. `:error` -> `js/console.error`, etc.
Defaults to `js.console.log` for unmatched signal levels. Defaults to `js.console.log` for unmatched signal levels.
NB: assumes that `js/console` exists, handler constructors should check first!" NB: assumes that `js/console` exists, handler constructors should check first!"
[level] [level]
(case level (case level
:trace js/console.trace :trace js/console.debug
:debug js/console.debug :debug js/console.debug
:info js/console.info :info js/console.info
:warn js/console.warn :warn js/console.warn
@ -159,8 +161,7 @@
(comment (js-console-logger)) (comment (js-console-logger))
(defn error-signal? (defn error-signal?
"Experimental, subject to change. "Returns true iff given signal has an `:error` value, or a `:kind` or `:level`
Returns true iff given signal has an `:error` value, or a `:kind` or `:level`
that indicates that it's an error." that indicates that it's an error."
#?(:cljs {:tag 'boolean}) #?(:cljs {:tag 'boolean})
[signal] [signal]
@ -205,7 +206,7 @@
[{:keys [type msg data]} ...] cause chain." [{:keys [type msg data]} ...] cause chain."
[signal] [signal]
(enc/if-let [error (get signal :error) (enc/if-let [error (get signal :error)
chain (enc/ex-chain :as-map error)] chain (truss/ex-chain :as-map error)]
(assoc signal :error chain) (assoc signal :error chain)
(do signal))) (do signal)))
@ -219,25 +220,34 @@
^java.io.File [file] ^java.io.File [file]
(let [file (as-file file)] (let [file (as-file file)]
(when-not (.exists file) (when-not (.exists file)
(when-let [parent (.getParentFile file)] (.mkdirs parent)) (truss/catching
(.createNewFile file)) (let [path (.toPath file)
fa (into-array java.nio.file.attribute.FileAttribute [])]
(when-let [parent (.getParent path)]
(do (java.nio.file.Files/createDirectories parent fa)))
(do (java.nio.file.Files/createFile path fa)))))
(if (.canWrite file) (if (.canWrite file)
file file
(throw (truss/ex-info! "Unable to prepare writable `java.io.File`"
(ex-info "Unable to prepare writable `java.io.File`" {:path (.getAbsolutePath file)})))))
{:path (.getAbsolutePath file)}))))))
(comment
(let [f (writeable-file! "__test-file.txt")]
(enc/qb 1e4 ; [10.27 37.69]
(.exists f)
(.canWrite f))))
#?(:clj #?(:clj
(defn ^:no-doc file-stream (defn ^:no-doc writeable-file-stream!
"Private, don't use. "Private, don't use.
Returns new `java.io.FileOutputStream` for given `java.io.File`." Returns new writeable `java.io.FileOutputStream` or throws."
^java.io.FileOutputStream [file append?] ^java.io.FileOutputStream [file append?]
(java.io.FileOutputStream. (as-file file) (boolean append?)))) (java.io.FileOutputStream. (writeable-file! file) (boolean append?))))
#?(:clj #?(:clj
(defn file-writer (defn file-writer
"Experimental, subject to change. "Alpha, subject to change.
Opens the specified file and returns a stateful fn of 2 arities: Opens the specified file and returns a stateful fn of 2 arities:
[content] => Writes given content to file, or noops if closed. [content] => Writes given content to file, or noops if closed.
[] => Closes the writer. [] => Closes the writer.
@ -253,10 +263,10 @@
[{:keys [file append?] [{:keys [file append?]
:or {append? true}}] :or {append? true}}]
(when-not file (throw (ex-info "Expected `:file` value" (enc/typed-val file)))) (when-not file (truss/ex-info! "Expected `:file` value" (truss/typed-val file)))
(let [file (writeable-file! file) (let [file (as-file file)
stream_ (volatile! (file-stream file append?)) stream_ (volatile! (writeable-file-stream! file append?))
open?_ (enc/latom true) open?_ (enc/latom true)
close! close!
@ -270,7 +280,7 @@
reset! reset!
(fn [] (fn []
(close!) (close!)
(vreset! stream_ (file-stream file append?)) (vreset! stream_ (writeable-file-stream! file append?))
(reset! open?_ true) (reset! open?_ true)
true) true)
@ -281,11 +291,11 @@
(.flush stream) (.flush stream)
true)) true))
file-exists! check-file!
(let [rl (enc/rate-limiter-once-per 100)] (let [rl (enc/rate-limiter-once-per 100)]
(fn [] (fn []
(or (rl) (.exists file) (or (rl) #_(.exists file) (.canWrite file)
(throw (java.io.IOException. "File doesn't exist"))))) (throw (java.io.IOException. "File doesn't exist or isn't writeable")))))
lock (Object.)] lock (Object.)]
@ -301,7 +311,7 @@
ba (enc/str->utf8-ba (str content))] ba (enc/str->utf8-ba (str content))]
(locking lock (locking lock
(try (try
(file-exists!) (check-file!)
(write-ba! ba) (write-ba! ba)
(catch java.io.IOException _ ; Retry once (catch java.io.IOException _ ; Retry once
(reset!) (reset!)
@ -342,10 +352,10 @@
Useful for basic handlers that write to a TCP socket, etc. Useful for basic handlers that write to a TCP socket, etc.
Options: Options:
`:ssl?` - Use SSL/TLS? `:ssl?` ------------------ Use SSL/TLS?
`:connect-timeout-msecs` - Connection timeout (default 3000 msecs) `:connect-timeout-msecs` - Connection timeout (default 3000 msecs)
`:socket-fn` - (fn [host port timeout]) => `java.net.Socket` `:socket-fn` ------------- (fn [host port timeout]) => `java.net.Socket`
`:ssl-socket-fn` - (fn [socket host port]) => `java.net.Socket` `:ssl-socket-fn` --------- (fn [socket host port]) => `java.net.Socket`
Notes: Notes:
- Writer should be manually closed after use (with zero-arity call). - Writer should be manually closed after use (with zero-arity call).
@ -364,8 +374,8 @@
socket-fn default-socket-fn socket-fn default-socket-fn
ssl-socket-fn default-ssl-socket-fn}}] ssl-socket-fn default-ssl-socket-fn}}]
(when-not (string? host) (throw (ex-info "Expected `:host` string" (enc/typed-val host)))) (when-not (string? host) (truss/ex-info! "Expected `:host` string" (truss/typed-val host)))
(when-not (int? port) (throw (ex-info "Expected `:port` int" (enc/typed-val port)))) (when-not (int? port) (truss/ex-info! "Expected `:port` int" (truss/typed-val port)))
(let [new-conn! ; => [<java.net.Socket> <java.io.OutputStream>], or throws (let [new-conn! ; => [<java.net.Socket> <java.io.OutputStream>], or throws
(fn [] (fn []
@ -379,7 +389,7 @@
[socket (.getOutputStream socket)]) [socket (.getOutputStream socket)])
(catch Exception ex (catch Exception ex
(throw (ex-info "Failed to create connection" opts ex))))) (truss/ex-info! "Failed to create connection" opts ex))))
conn_ (volatile! (new-conn!)) conn_ (volatile! (new-conn!))
open?_ (enc/latom true) open?_ (enc/latom true)
@ -440,7 +450,7 @@
;;;; Formatters ;;;; Formatters
(defn format-nsecs-fn (defn format-nsecs-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn format [nanosecs]) that: Returns a (fn format [nanosecs]) that:
- Takes a long nanoseconds (e.g. runtime). - Takes a long nanoseconds (e.g. runtime).
- Returns a human-readable string like: - Returns a human-readable string like:
@ -464,10 +474,10 @@
(s+nl " " class "/" method " at " file ":" line))) (s+nl " " class "/" method " at " file ":" line)))
(str sb)))) (str sb))))
(comment (println (format-clj-stacktrace (:trace (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))))) (comment (println (format-clj-stacktrace (:trace (truss/ex-map (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"})))))))
(defn format-error-fn (defn format-error-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn format [error]) that: Returns a (fn format [error]) that:
- Takes a platform error (`Throwable` or `js/Error`). - Takes a platform error (`Throwable` or `js/Error`).
- Returns a human-readable error string." - Returns a human-readable error string."
@ -477,17 +487,17 @@
nls enc/newlines] nls enc/newlines]
(fn format-error [error] (fn format-error [error]
(when-let [em (enc/ex-map error)] (when-let [em (truss/ex-map error)]
(let [sb (enc/str-builder) (let [sb (enc/str-builder)
s+ (partial enc/sb-append sb) s+ (partial enc/sb-append sb)
{:keys [chain trace]} em] {:keys [chain trace]} em]
(let [s+cause (enc/sb-appender sb (str nls "Caused: "))] (let [s+cause (enc/sb-appender sb (str nls "Caused: "))]
(s+ " Root: ") (s+ "Root: ")
(doseq [{:keys [type msg data]} (rseq chain)] (doseq [{:keys [type msg data]} (rseq chain)]
(s+cause type " - " msg) (s+cause type " - " msg)
(when data (when data
(s+ nl " data: " (enc/pr-edn* data))))) (s+ nl "data: " (enc/pr-edn* data)))))
(when trace (when trace
(s+ nl nl "Root stack trace:" nl) (s+ nl nl "Root stack trace:" nl)
@ -497,24 +507,28 @@
(str sb))))))) (str sb)))))))
(comment (comment
(do (throw (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})))) (do (throw (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"}))))
(do (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})))) (do (truss/ex-map (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"}))))
(println (str "--\n" ((format-error-fn) (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})))))) (println (str "--\n" ((format-error-fn) (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"}))))))
;;;; ;;;;
(defn signal-preamble-fn (defn signal-preamble-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn preamble [signal]) that: Returns a (fn preamble [signal]) that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Returns a signal preamble ?string like: - Returns a signal preamble ?string like:
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere(2,21) ::ev-id - msg\" \"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere[2,21] ::ev-id msg\"
Options: Options:
`:format-inst-fn` - (fn format [instant]) => string." `:format-inst-fn` - (fn format [instant]) => string.
`:format-id-fn` --- (fn format [ns id]) => string.
`:format-msg-fn` -- (fn format [msg]) => string."
([] (signal-preamble-fn nil)) ([] (signal-preamble-fn nil))
([{:keys [format-inst-fn] ([{:keys [format-inst-fn format-id-fn format-msg-fn]
:or {format-inst-fn (format-inst-fn)}}] :or {format-inst-fn (format-inst-fn)
format-id-fn format-id
format-msg-fn identity}}]
(fn signal-preamble [signal] (fn signal-preamble [signal]
(let [{:keys [inst level kind ns id msg_]} signal (let [{:keys [inst level kind ns id msg_]} signal
@ -523,37 +537,44 @@
(when inst (when-let [ff format-inst-fn] (s+spc (ff inst)))) (when inst (when-let [ff format-inst-fn] (s+spc (ff inst))))
(when level (s+spc (format-level level))) (when level (s+spc (format-level level)))
(when kind (s+spc (sigs/upper-qn kind)))
(if kind (s+spc (upper-qn kind)) (s+spc "DEFAULT")) #?(:clj
#?(:clj (s+spc (hostname))) (when-let [hostname (enc/get-in* signal [:host :name])]
(s+spc hostname)))
;; "<ns>(<line>,<column>)" (when ns (s+spc (sigs/format-callsite ns (get signal :coords))))
(when-let [base (or ns (get signal :file))] (when id (when-let [ff format-id-fn] (s+spc (ff ns id))))
(let [s+ (partial enc/sb-append sb)] ; Without separator (enc/when-let [ff format-msg-fn
(s+ " " base) msg (force msg_)]
(when-let [l (get signal :line)] (s+spc (ff msg)))
(s+ "(" l)
(when-let [c (get signal :column)] (s+ "," c))
(s+ ")"))))
(when id (s+spc (format-id ns id)))
(when-let [msg (force msg_)] (s+spc "- " msg))
(when-not (zero? (enc/sb-length sb)) (when-not (zero? (enc/sb-length sb))
(str sb)))))) (str sb))))))
(comment ((signal-preamble-fn) (tel/with-signal (tel/event! ::ev-id)))) (comment ((signal-preamble-fn) (tel/with-signal (tel/event! ::ev-id))))
(defn- format-parent [ns {:keys [id uid]}]
(if id
(if uid
{:id (symbol (format-id ns id)), :uid uid}
{:id (symbol (format-id ns id))})
(if uid
{:uid uid}
nil)))
(comment (str (format-parent (str *ns*) {:id ::id1 :uid "uid1"})))
(defn signal-content-fn (defn signal-content-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn content [signal]) that: Returns a (fn content [signal]) that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Returns a signal content ?string (incl. data, ctx, etc.). - Returns a human-readable signal content ?string (incl. data, ctx, etc.).
Options: Options:
`:raw-error?` - Retain unformatted error? (default false) `:raw-error?` ------ Retain unformatted error? (default false)
`:incl-keys` - Subset of signal keys to retain from those `:incl-keys` ------- Subset of signal keys to retain from those
otherwise excluded by default: #{:kvs :thread} otherwise excluded by default: #{:kvs :host :thread}
`:format-nsecs-fn` - (fn [nanosecs]) => string. `:format-nsecs-fn` - (fn [nanosecs]) => string.
`:format-error-fn` - (fn [error]) => string." `:format-error-fn` - (fn [error]) => string."
@ -568,6 +589,7 @@
err-start (str nl "<<< error <<<" nl) err-start (str nl "<<< error <<<" nl)
err-stop (str nl ">>> error >>>") err-stop (str nl ">>> error >>>")
incl-kvs? (contains? incl-keys :kvs) incl-kvs? (contains? incl-keys :kvs)
incl-host? (contains? incl-keys :host)
incl-thread? (contains? incl-keys :thread)] incl-thread? (contains? incl-keys :thread)]
(fn signal-content (fn signal-content
@ -583,16 +605,17 @@
(let [af append-fn (let [af append-fn
vf val-fn] vf val-fn]
(let [{:keys [uid parent root data kvs ctx #?@(:clj [host thread]) sample-rate]} signal] (let [{:keys [ns uid parent root data kvs ctx #?@(:clj [host thread]) sample]} signal]
(when sample-rate (af " sample: " (vf sample-rate))) (when sample (af " sample: " (vf sample)))
(when uid (af " uid: " (vf uid))) (when uid (af " uid: " (vf uid)))
(when parent (af " parent: " (vf (dissoc parent :inst)))) ; {:keys [id uid]} (when (and parent (not= parent root)) (af " parent: " (vf (format-parent ns parent)))) ; {:keys [id uid]}
(when (and parent root) (af " root: " (vf (dissoc root :inst)))) ; {:keys [id uid]} (when root (af " root: " (vf (format-parent ns root)))) ; {:keys [id uid]}
#?(:clj (when host (af " host: " (vf host)))) ; {:keys [ name ip]}
#?(:clj (when (and thread incl-thread?) (af " thread: " (vf thread)))) ; {:keys [group name id]} #?(:clj (when (enc/and? host incl-host?) (af " host: " (vf host)))) ; {:keys [ name ip]}
(when data (af " data: " (vf data))) #?(:clj (when (enc/and? thread incl-thread?) (af " thread: " (vf thread)))) ; {:keys [group name id]}
(when (and kvs incl-kvs?) (af " kvs: " (vf kvs))) (when (enc/not-empty-coll data) (af " data: " (vf data)))
(when ctx (af " ctx: " (vf ctx)))) (when (enc/not-empty-coll ctx) (af " ctx: " (vf ctx)))
(when (enc/and? kvs incl-kvs?) (af " kvs: " (vf kvs))))
(let [{:keys [run-form error]} signal] (let [{:keys [run-form error]} signal]
(when run-form (when run-form
@ -621,125 +644,155 @@
((signal-content-fn) (tel/with-signal (tel/event! ::ev-id))) ((signal-content-fn) (tel/with-signal (tel/event! ::ev-id)))
((signal-content-fn) (tel/with-signal (tel/event! ::ev-id {:data {:k1 "v1"}})))) ((signal-content-fn) (tel/with-signal (tel/event! ::ev-id {:data {:k1 "v1"}}))))
(defn pr-signal-fn (defn clean-signal-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn pr [signal]) that: Returns a (fn clean [signal]) that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Returns a machine-readable signal ?string. - Returns a minimal signal (map) ready for printing, etc.
Signals are optimized for cheap creation and easy handling, so tend to be
verbose and may contain things like nil values and duplicated content.
This util efficiently cleans signals of such noise, helping reduce
storage/transmission size, and making key info easier to see.
Options: Options:
`:pr-fn` - ∈ #{<unary-fn> :edn (default) :json (Cljs only)}
`:incl-kvs?` - Include signal's app-level kvs? (default false)
`:incl-nils?` - Include signal's keys with nil values? (default false) `:incl-nils?` - Include signal's keys with nil values? (default false)
`:incl-kvs?` -- Include signal's app-level root kvs? (default false)
`:incl-keys` -- Subset of signal keys to retain from those otherwise
excluded by default: #{:schema :kvs :host :thread}"
([] (clean-signal-fn nil))
([{:keys [incl-kvs? incl-nils? incl-keys] :as opts}]
(let [assoc!*
(if-not incl-nils?
(fn [m k v] (if (nil? v) m (assoc! m k v))) ; As `remove-signal-nils`
(do assoc!))
incl-schema? (contains? incl-keys :schema)
incl-kvs-key? (contains? incl-keys :kvs)
incl-host? (contains? incl-keys :host)
incl-thread? (contains? incl-keys :thread)]
(fn clean-signal [signal]
(when (map? signal)
(persistent!
(reduce-kv
(fn [m k v]
(enc/case-eval k
;; Main keys to always include as-is
(clojure.core/into ()
(clojure.core/disj
taoensso.telemere.impl/standard-signal-keys
:msg_ :error :schema :kvs :host :thread))
(assoc!* m k v)
;; Main keys to include with modified val
:error (if-let [chain (truss/ex-chain :as-map v)] (assoc! m k chain) m) ; As `expand-signal-error`
:msg_ (assoc!* m k (force v)) ; As `force-signal-msg`
;; Implementation keys to always exclude
(clojure.core/into ()
taoensso.telemere.impl/impl-signal-keys) m ; noop
;;; Other keys to exclude by default
:schema (if incl-schema? (assoc!* m k v) m)
:kvs (if incl-kvs-key? (assoc!* m k v) m)
:thread (if incl-thread? (assoc!* m k v) m)
:host (if incl-host? (assoc!* m k v) m)
;; Other (app-level) keys
(enc/cond
incl-kvs? (assoc!* m k v) ; Incl. all kvs
(contains? incl-keys k) (assoc!* m k v) ; Incl. specific kvs
:else m ; As `remove-signal-kvs`
)))
(transient {}) signal)))))))
(comment ((clean-signal-fn {:incl-keys #{:a}}) {:level :info, :id nil, :a "a", :b "b", :msg_ (delay "hi")}))
(defn pr-signal-fn
"Alpha, subject to change.
Returns a (fn pr [signal]) that:
- Takes a Telemere signal (map).
- Returns a machine-readable signal string.
Options:
`:pr-fn` --------- ∈ #{<unary-fn> :edn (default) :json (Cljs only)}
`:clean-fn` ------ (fn [signal]) => clean signal map, see [1]
`:incl-newline?` - Include terminating system newline? (default true) `:incl-newline?` - Include terminating system newline? (default true)
`:incl-keys` - Subset of signal keys to retain from those otherwise
excluded by default: #{:location :kvs :file :thread}
Examples: Examples:
(pr-signal-fn {:pr-fn :edn ...}) ; Outputs edn
(pr-signal-fn {:pr-fn :json ...}) ; Outputs JSON (Cljs only)
To output JSON for Clj, you must provide an appropriate `:pr-fn`. ;; To print as edn:
`jsonista` offers one good option, Ref. <https://github.com/metosin/jsonista>: (pr-signal-fn {:pr-fn :edn})
(require '[jsonista.core :as jsonista]) ;; To print as JSON:
(pr-signal-fn {:pr-fn jsonista/write-value-as-string ...}) ;; Ref. <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista]))
(pr-signal-fn
{:pr-fn
#?(:cljs :json ; Use js/JSON.stringify
:clj jsonista/write-value-as-string)})
See also `format-signal-fn` for human-readable output." [1] `taoensso.telemere.utils/clean-signal-fn`, etc.
See also `format-signal-fn` for an alternative to `pr-signal-fn`
that produces human-readable output."
([] (pr-signal-fn nil)) ([] (pr-signal-fn nil))
([{:keys [pr-fn, incl-kvs? incl-nils? incl-newline? incl-keys] :as opts ([{:keys [pr-fn clean-fn incl-newline?] :as opts
:or :or
{pr-fn :edn {pr-fn :edn
clean-fn (clean-signal-fn)
incl-newline? true}}] incl-newline? true}}]
(let [nl newline (let [nl newline
pr-fn pr-fn
(or (or
(case pr-fn (case pr-fn
:none nil ; Undocumented, used for unit tests
:edn pr-edn :edn pr-edn
:json :json
#?(:cljs pr-json #?(:cljs pr-json
:clj :clj
(throw (truss/ex-info! "`:json` pr-fn only supported in Cljs. To output JSON in Clj, please provide an appropriate unary fn instead (e.g. jsonista/write-value-as-string)."
(ex-info "`:json` pr-fn only supported in Cljs. To output JSON in Clj, please provide an appropriate unary fn instead (e.g. jsonista/write-value-as-string)." {}))
{})))
(if (fn? pr-fn) (if (fn? pr-fn)
(do pr-fn) (do pr-fn)
(enc/unexpected-arg! pr-fn (truss/unexpected-arg! pr-fn
{:context `pr-signal-fn {:param 'pr-fn
:param 'pr-fn :context `pr-signal-fn
:expected :expected
#?(:clj '#{:edn unary-fn} #?(:clj '#{:edn unary-fn}
:cljs '#{:edn :json unary-fn})})))) :cljs '#{:edn :json unary-fn})}))))]
assoc!*
(if-not incl-nils?
(fn [m k v] (if (nil? v) m (assoc! m k v))) ; As `remove-signal-nils`
(do assoc!))
incl-location? (contains? incl-keys :location)
incl-kvs-key? (contains? incl-keys :kvs)
incl-file? (contains? incl-keys :file)
incl-thread? (contains? incl-keys :thread)]
(fn pr-signal [signal] (fn pr-signal [signal]
(when (map? signal) (when (map? signal)
(let [signal
(persistent!
(reduce-kv
(fn [m k v]
(enc/case-eval k
:msg_ (assoc!* m k (force v)) ; As force-signal-msg
:error (if-let [chain (enc/ex-chain :as-map v)] (assoc! m k chain) m) ; As expand-signal-error
;;; Keys excluded by default
:location (if incl-location? (assoc!* m k v) m)
:kvs (if incl-kvs-key? (assoc!* m k v) m)
:file (if incl-file? (assoc!* m k v) m)
:thread (if incl-thread? (assoc!* m k v) m)
(clojure.core/into ()
(clojure.core/disj
taoensso.telemere.impl/standard-signal-keys
:msg_ :error :location :kvs :file :thread))
(assoc!* m k v)
(if incl-kvs? (assoc!* m k v) m) ; As remove-signal-kvs
))
(transient {}) signal))]
(if-not pr-fn
signal
(let [output (pr-fn signal)]
(if incl-newline? (if incl-newline?
(str output nl) (str (pr-fn (clean-fn signal)) nl)
(do output)))))))))) (do (pr-fn (clean-fn signal)))))))))
(comment (comment
(def s1 (tel/with-signal (tel/event! ::ev-id {:kvs {:k1 "v1"}}))) ((pr-signal-fn {:pr-fn :edn})
((pr-signal-fn {:pr-fn :edn}) s1) (tel/with-signal (tel/event! ::ev-id {:kvs {:k1 "v1"}}))))
((pr-signal-fn {:pr-fn (fn [_] "str")}) s1)
((pr-signal-fn {:pr-fn :none}) s1)
(let [pr-fn (pr-signal-fn {:pr-fn :none})]
(enc/qb 1e6 ; 817.78
(pr-fn s1))))
(defn format-signal-fn (defn format-signal-fn
"Experimental, subject to change. "Alpha, subject to change.
Returns a (fn format [signal]) that: Returns a (fn format [signal]) that:
- Takes a Telemere signal (map). - Takes a Telemere signal (map).
- Returns a human-readable signal string. - Returns a human-readable signal string.
Options: Options:
`:incl-newline?` - Include terminating system newline? (default true) `:incl-newline?` - Include terminating system newline? (default true)
`:preamble-fn` - (fn [signal]) => signal preamble string. `:preamble-fn` --- (fn [signal]) => signal preamble string, see [1]
`:content-fn` - (fn [signal]) => signal content string. `:content-fn` ---- (fn [signal]) => signal content string, see [2]
[1] `taoensso.telemere.utils/signal-preamble-fn`, etc.
[2] `taoensso.telemere.utils/signal-content-fn`, etc.
See also `pr-signal-fn` for an alternative to `format-signal-fn`
that produces machine-readable output (edn, JSON, etc.)."
See also `pr-signal-fn` for machine-readable output."
([] (format-signal-fn nil)) ([] (format-signal-fn nil))
([{:keys [incl-newline? preamble-fn content-fn] ([{:keys [incl-newline? preamble-fn content-fn]
:or :or
@ -765,5 +818,5 @@
{:my-k1 #{:a :b :c} {:my-k1 #{:a :b :c}
:msg "hi" :msg "hi"
:data {:a :A} :data {:a :A}
;; :error (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})) ;; :error (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"}))
:run (/ 1 0)})))))) :run (/ 1 0)}))))))

File diff suppressed because it is too large Load diff

View file

@ -1,57 +0,0 @@
Telemere supports extensive environmental config via JVM properties,
environment variables, or classpath resources.
Environmental filter config includes:
Kind filter:
JVM property: `taoensso.telemere.rt-kind-filter.edn`
Env variable: `TAOENSSO_TELEMERE_RT_KIND_FILTER_EDN`
Classpath resource: `taoensso.telemere.rt-kind-filter.edn`
Namespace filter:
JVM property: `taoensso.telemere.rt-ns-filter.edn`
Env variable: `TAOENSSO_TELEMERE_RT_NS_FILTER_EDN`
Classpath resource: `taoensso.telemere.rt-ns-filter.edn`
Id filter:
JVM property: `taoensso.telemere.rt-id-filter.edn`
Env variable: `TAOENSSO_TELEMERE_RT_ID_FILTER_EDN`
Classpath resource: `taoensso.telemere.rt-id-filter.edn`
Minimum level:
JVM property: `taoensso.telemere.rt-min-level.edn`
Env variable: `TAOENSSO_TELEMERE_RT_MIN_LEVEL_EDN`
Classpath resource: `taoensso.telemere.rt-min-level.edn`
Examples:
`taoensso.telemere.rt-min-level.edn` -> ":info"
`TAOENSSO_TELEMERE_RT_NS_FILTER_EDN` -> "{:disallow \"taoensso.*\"}"
`taoensso.telemere.rt-id-filter.cljs.edn` -> "#{:my-id1 :my-id2}"
`TAOENSSO_TELEMERE_RT_KIND_FILTER_CLJ_EDN` -> "nil"
For other (non-filter) environmental config, see the relevant docstrings.
Tips:
- The above ids are for runtime filters (the most common).
For compile-time filters, change `rt`->`ct` / `RT`->`CT`.
- The above ids will affect both Clj AND Cljs.
For platform-specific filters, use
".clj.edn" / "_CLJ_EDN" or
".cljs.edn" / "_CLJS_EDN" suffixes instead.
- ".edn" / "_EDN" suffixes are optional.
- Config values should be edn. To get the right syntax, first set
your runtime filters using the standard utils (`set-min-level!`,
etc.). Then call `get-filters` and serialize the relevant parts
to edn with `pr-str`.
- All environmental config uses `get-env` underneath.
See the `get-env` docstring for more/advanced details.
- Classpath resources are files accessible on your project's
classpath. This usually includes files in your project's
`resources/` dir.

View file

@ -1,38 +0,0 @@
Signal options (shared by all signal creators):
`:inst` -------- Platform instant [1] when signal was created, ∈ #{nil :auto <[1]>}
`:level` ------- Signal level ∈ #{<int> :trace :debug :info :warn :error :fatal :report ...}
`:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy <app-val> ...}
`:id` ---------- ?id of signal (common to all signals created at callsite, contrast with `:uid`)
`:uid` --------- ?id of signal instance (unique to each signal created at callsite, contrast with `:id`)
`:msg` --------- Arb app-level ?message to incl. in signal: str or vec of strs to join (with `\space`)
`:data` -------- Arb app-level ?data to incl. in signal: usu. a map
`:error` ------- Arb app-level ?error to incl. in signal: platform error [2]
`:run` --------- ?form to execute UNCONDITIONALLY; will incl. `:run-value` in signal
`:do` ---------- ?form to execute conditionally (iff signal allowed), before establishing `:let` ?binding
`:let` --------- ?bindings to establish conditionally (iff signal allowed), BEFORE evaluating `:data` and `:msg` (useful!)
`:ctx` --------- Custom ?val to override auto (dynamic `*ctx*`) in signal
`:parent` ------ Custom ?{:keys [id uid]} to override auto (dynamic) parent signal tracing info
`:root` -------- Custom ?{:keys [id uid]} to override auto (dynamic) root signal tracing info
`:location` ---- Custom ?{:keys [ns line column file]} to override auto signal creator callsite location
`:elidable?` --- Should signal be subject to compile-time elision? (Default: true)
`: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` -- Optional (fn [signal]) => ?modified-signal to apply when signal is created
`:trace?` ------ Should tracing be enabled for `:run` form?
<kvs> ---------- Other arb app-level ?kvs to incl. in signal. Typically NOT included in
handler output, so a great way to provide custom data/opts for use
(only) by custom middleware/handlers.
handler-specific data that can just be ignored by other handlers
If anything is unclear, please ping me (@ptaoussanis) so that I can improve these docs!
[1] `java.time.Instant` or `js/Date`
[2] `java.lang.Throwable` or `js/Error`

View file

@ -1,41 +0,0 @@
"Spy" signal creator, emphasizing form + level.
API: [form] [level-or-opts form] => form's result (value/throw) (unconditional)
Default kind: `:spy`
Default level: `:info`
When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to
console/file/queue/db, etc.).
Examples:
(spy! (+ 1 2)) ; %> {:kind :trace, :level :info, :run-form '(+ 1 2),
; :run-val 3, :run-nsecs <int>, :parent {:keys [id uid]}
; :msg "(+ 1 2) => 3" ...}
(spy! ::my-id (+ 1 2)) ; %> {... :id ::my-id ...}
(spy!
{:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}}
(+ 1 2)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
Tips:
- Test using `with-signal`: (with-signal (spy! ...)).
- Supports the same options [2] as other signals [1].
- Identical to `trace!`, but emphasizes form + level rather than form + id.
- Useful for debugging/monitoring forms, and tracing (nested) execution flow.
- Execution of `form` arg may create additional (nested) signals.
Each signal's `:parent` key will indicate its immediate parent.
- Can be useful to wrap with `catch->error!`:
(catch->error! ::error-id (spy! ...)).
----------------------------------------------------------------------
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
[2] See `help:signal-options` - {:keys [kind level id data ...]}
[3] See `help:signal-content` - {:keys [kind level id data ...]}
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)

View file

@ -1,45 +0,0 @@
"Trace" signal creator, emphasizing form + id.
API: [form] [id-or-opts form] => form's result (value/throw) (unconditional)
Default kind: `:trace`
Default level: `:info` (intentionally NOT `:trace`!)
When filtering conditions are met [4], creates a Telemere signal [3] and
dispatches it to registered handlers for processing (e.g. writing to
console/file/queue/db, etc.).
Examples:
(trace! (+ 1 2)) ; %> {:kind :trace, :level :info, :run-form '(+ 1 2),
; :run-val 3, :run-nsecs <int>, :parent {:keys [id uid]} ...
; :msg "(+ 1 2) => 3" ...}
(trace! ::my-id (+ 1 2)) ; %> {... :id ::my-id ...}
(trace!
{:let [x "x"] ; Available to `:data` and `:msg`
:data {:x x}}
(+ 1 2)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
Tips:
- Test using `with-signal`: (with-signal (trace! ...)).
- Supports the same options [2] as other signals [1].
- Identical to `spy!`, but emphasizes form + id rather than form + level.
- Useful for debugging/monitoring forms, and tracing (nested) execution flow.
- Execution of `form` arg may create additional (nested) signals.
Each signal's `:parent` key will indicate its immediate parent.
- Can be useful to wrap with `catch->error!`:
(catch->error! ::error-id (trace! ...)).
- Default level is `:info`, not `:trace`! The name "trace" in "trace signal"
refers to the general action of tracing program flow rather than to the
common logging level of the same name.
----------------------------------------------------------------------
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
[2] See `help:signal-options` - {:keys [kind level id data ...]}
[3] See `help:signal-content` - {:keys [kind level id data ...]}
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)

1
slf4j/.gitignore vendored
View file

@ -13,4 +13,3 @@ pom.xml*
/.clj-kondo/.cache /.clj-kondo/.cache
.idea/ .idea/
*.iml *.iml
/wiki/.git

View file

@ -1,4 +1,4 @@
(defproject com.taoensso/slf4j-telemere "1.0.0-beta18" (defproject com.taoensso/telemere-slf4j "1.2.1"
:author "Peter Taoussanis <https://www.taoensso.com>" :author "Peter Taoussanis <https://www.taoensso.com>"
:description "Telemere backend/provider for SLF4J API v2" :description "Telemere backend/provider for SLF4J API v2"
:url "https://www.taoensso.com/telemere" :url "https://www.taoensso.com/telemere"
@ -7,16 +7,18 @@
{:name "Eclipse Public License - v 1.0" {:name "Eclipse Public License - v 1.0"
:url "https://www.eclipse.org/legal/epl-v10.html"} :url "https://www.eclipse.org/legal/epl-v10.html"}
:scm {:name "git" :url "https://github.com/taoensso/telemere"}
:java-source-paths ["src/java"] :java-source-paths ["src/java"]
:javac-options ["--release" "11" "-g"] ; Support Java >= v11 :javac-options ["--release" "8" "-g"] ; Support Java >= v8
:dependencies [] :dependencies []
:profiles :profiles
{:provided {:provided
{:dependencies {:dependencies
[[org.clojure/clojure "1.11.4"] [[org.clojure/clojure "1.12.3"]
[org.slf4j/slf4j-api "2.0.16"] [org.slf4j/slf4j-api "2.0.17"]
[com.taoensso/telemere "1.0.0-beta18"]]} [com.taoensso/telemere "1.2.1"]]}
:dev :dev
{:plugins {:plugins

View file

@ -40,15 +40,7 @@ public class TelemereLoggerFactory implements ILoggerFactory {
TelemereLogger.lazyInit(); TelemereLogger.lazyInit();
} }
public Logger getLogger(String name) { public Logger getLogger(String name) { return loggerMap.computeIfAbsent(name, this::createLogger); }
return loggerMap.computeIfAbsent(name, this::createLogger); protected Logger createLogger(String name) { return new TelemereLogger(name); }
} protected void reset() { loggerMap.clear(); }
protected Logger createLogger(String name) {
return new TelemereLogger(name);
}
protected void reset() {
loggerMap.clear();
}
} }

View file

@ -1,9 +1,9 @@
(ns taoensso.telemere.slf4j (ns taoensso.telemere.slf4j
"Intake support for SLF4J -> Telemere. "SLF4Jv2 -> Telemere interop.
Telemere will attempt to load this ns automatically when possible. Telemere will attempt to load this ns automatically when possible.
To use Telemere as your SLF4J backend/provider, just include the To use Telemere as your SLF4J backend/provider, just include the
`com.taoensso/slf4j-telemere` dependency on your classpath. `com.taoensso/telemere-slf4j` dependency on your classpath.
Implementation details, Implementation details,
Ref. <https://www.slf4j.org/faq.html#slf4j_compatible>: Ref. <https://www.slf4j.org/faq.html#slf4j_compatible>:
@ -11,19 +11,23 @@
- Libs must include `org.slf4j/slf4j-api` dependency, but NO backend. - Libs must include `org.slf4j/slf4j-api` dependency, but NO backend.
- Users must include a single backend dependency of their choice - Users must include a single backend dependency of their choice
(e.g. `com.taoensso/slf4j-telemere` or `org.slf4j/slf4j-simple`). (e.g. `com.taoensso/telemere-slf4j` or `org.slf4j/slf4j-simple`).
- SLF4J uses standard `ServiceLoader` mechanism to find its logging backend, - SLF4J uses standard `ServiceLoader` mechanism to find its logging backend,
searches for `SLF4JServiceProvider` provider on classpath." searches for `SLF4JServiceProvider` provider on classpath."
{:author "Peter Taoussanis (@ptaoussanis)"}
(:require (:require
[taoensso.encore :as enc :refer [have have?]] [taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.telemere.impl :as impl]) [taoensso.telemere.impl :as impl])
(:import (:import
[org.slf4j Logger] [org.slf4j Logger]
[com.taoensso.telemere.slf4j TelemereLogger])) [com.taoensso.telemere.slf4j TelemereLogger]))
(comment (remove-ns (symbol (str *ns*))))
;;;; Utils ;;;; Utils
(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body))) (defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))
@ -40,7 +44,7 @@
org.slf4j.event.EventConstants/ERROR_INT :error org.slf4j.event.EventConstants/ERROR_INT :error
(throw (throw
(ex-info "Unexpected `org.slf4j.event.Level`" (ex-info "Unexpected `org.slf4j.event.Level`"
{:level {:value level, :type (type level)}})))) {:level (enc/typed-val level)}))))
(comment (enc/qb 1e6 (sig-level org.slf4j.event.Level/INFO))) ; 36.47 (comment (enc/qb 1e6 (sig-level org.slf4j.event.Level/INFO))) ; 36.47
@ -59,13 +63,13 @@
(comment [(est-marker! "a1" "a2") (get-marker "a1") (= (get-marker "a1") (get-marker "a1"))]) (comment [(est-marker! "a1" "a2") (get-marker "a1") (= (get-marker "a1") (get-marker "a1"))])
(def ^:private marker-names (def ^:private get-marker-names
"Returns #{<MarkerName>}. Cached => assumes markers NOT modified after creation." "Returns #{<MarkerName>}. Cached => assumes markers NOT modified after creation."
;; We use `BasicMarkerFactory` so: ;; We use `BasicMarkerFactory` so:
;; 1. Our markers are just labels (no other content besides their name). ;; 1. Our markers are just labels (no other content besides their name).
;; 2. Markers with the same name are identical (enabling caching). ;; 2. Markers with the same name are identical (enabling caching).
(enc/fmemoize (enc/fmemoize
(fn marker-names [marker-or-markers] (fn get-marker-names [marker-or-markers]
(if (instance? org.slf4j.Marker marker-or-markers) (if (instance? org.slf4j.Marker marker-or-markers)
;; Single marker ;; Single marker
@ -78,13 +82,13 @@
(fn [acc ^org.slf4j.Marker in] (fn [acc ^org.slf4j.Marker in]
(if-not (.hasReferences in) (if-not (.hasReferences in)
(conj acc (.getName in)) (conj acc (.getName in))
(into acc (marker-names in)))) (into acc (get-marker-names in))))
acc (.iterator m)))) acc (.iterator m))))
;; Vector of markers ;; Vector of markers
(reduce (reduce
(fn [acc in] (into acc (marker-names in))) (fn [acc in] (into acc (get-marker-names in)))
#{} (have vector? marker-or-markers)))))) #{} (truss/have vector? marker-or-markers))))))
(comment (comment
(let [m1 (est-marker! "M1") (let [m1 (est-marker! "M1")
@ -93,19 +97,18 @@
ms [m1 m2]] ms [m1 m2]]
(enc/qb 1e6 ; [45.52 47.48 44.85] (enc/qb 1e6 ; [45.52 47.48 44.85]
(marker-names m1) (get-marker-names m1)
(marker-names cm) (get-marker-names cm)
(marker-names ms)))) (get-marker-names ms))))
;;;; Intake fns (called by `TelemereLogger`) ;;;; Interop fns (called by `TelemereLogger`)
(defn- allowed? (defn- allowed?
"Private, don't use. "Called by `com.taoensso.telemere.slf4j.TelemereLogger`."
Called by `com.taoensso.telemere.slf4j.TelemereLogger`."
[logger-name level] [logger-name level]
(when-debug (println [:slf4j/allowed? (sig-level level) logger-name])) (when-debug (println [:slf4j/allowed? (sig-level level) logger-name]))
(impl/signal-allowed? (impl/signal-allowed?
{:location {:ns logger-name} ; Typically source class name {:ns logger-name ; Typically source class name
:kind :slf4j :kind :slf4j
:level (sig-level level)})) :level (sig-level level)}))
@ -114,13 +117,13 @@
(when-debug (println [:slf4j/normalized-log! (sig-level level) logger-name])) (when-debug (println [:slf4j/normalized-log! (sig-level level) logger-name]))
(impl/signal! (impl/signal!
{:allow? true ; Pre-filtered by `allowed?` call {:allow? true ; Pre-filtered by `allowed?` call
:location {:ns logger-name} ; Typically source class name :ns logger-name ; Typically source class name
:kind :slf4j :kind :slf4j
:level (sig-level level) :level (sig-level level)
:inst inst :inst inst
:error error :error error
:ctx :ctx+
(when-let [hmap (org.slf4j.MDC/getCopyOfContextMap)] (when-let [hmap (org.slf4j.MDC/getCopyOfContextMap)]
(clojure.lang.PersistentHashMap/create hmap)) (clojure.lang.PersistentHashMap/create hmap))
@ -129,16 +132,14 @@
(org.slf4j.helpers.MessageFormatter/basicArrayFormat (org.slf4j.helpers.MessageFormatter/basicArrayFormat
msg-pattern args)) msg-pattern args))
:data :slf4j/args args ; Object[]
(enc/assoc-some nil :slf4j/markers marker-names ; Usu. used for routing, filtering, xfns, etc.
:slf4j/marker-names marker-names :data (when kvs {:slf4j/kvs kvs})})
:slf4j/args (when args (vec args))
:slf4j/kvs kvs)})
nil) nil)
(defn- log! (defn- log!
"Private, don't use. "Called by `com.taoensso.telemere.slf4j.TelemereLogger`."
Called by `com.taoensso.telemere.slf4j.TelemereLogger`."
;; Modern "fluent" API calls ;; Modern "fluent" API calls
([logger-name ^org.slf4j.event.LoggingEvent event] ([logger-name ^org.slf4j.event.LoggingEvent event]
@ -147,7 +148,7 @@
error (.getThrowable event) error (.getThrowable event)
msg-pattern (.getMessage event) msg-pattern (.getMessage event)
args (when-let [args (.getArgumentArray event)] args) args (when-let [args (.getArgumentArray event)] args)
markers (when-let [markers (.getMarkers event)] (marker-names (vec markers))) marker-names (when-let [markers (.getMarkers event)] (get-marker-names (vec markers)))
kvs (when-let [kvps (.getKeyValuePairs event)] kvs (when-let [kvps (.getKeyValuePairs event)]
(reduce (reduce
(fn [acc ^org.slf4j.event.KeyValuePair kvp] (fn [acc ^org.slf4j.event.KeyValuePair kvp]
@ -155,11 +156,11 @@
nil kvps))] nil kvps))]
(when-debug (println [:slf4j/fluent-log-call (sig-level level) logger-name])) (when-debug (println [:slf4j/fluent-log-call (sig-level level) logger-name]))
(normalized-log! logger-name level inst error msg-pattern args markers kvs))) (normalized-log! logger-name level inst error msg-pattern args marker-names kvs)))
;; Legacy API calls ;; Legacy API calls
([logger-name ^org.slf4j.event.Level level error msg-pattern args marker] ([logger-name ^org.slf4j.event.Level level error msg-pattern args marker]
(let [marker-names (when marker (marker-names marker))] (let [marker-names (when marker (get-marker-names marker))]
(when-debug (println [:slf4j/legacy-log-call (sig-level level) logger-name])) (when-debug (println [:slf4j/legacy-log-call (sig-level level) logger-name]))
(normalized-log! logger-name level (enc/now-inst*) error msg-pattern args marker-names nil)))) (normalized-log! logger-name level (enc/now-inst*) error msg-pattern args marker-names nil))))
@ -176,25 +177,26 @@
;;;; ;;;;
(defn check-intake (defn check-interop
"Returns {:keys [present? sending->telemere? telemere-receiving?]}." "Returns interop debug info map."
[] []
(let [^org.slf4j.Logger sl (let [^org.slf4j.Logger sl
(org.slf4j.LoggerFactory/getLogger "IntakeTestTelemereLogger") (org.slf4j.LoggerFactory/getLogger "InteropTestTelemereLogger")
sending? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl) sending? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl)
receiving? receiving?
(and sending? (and sending?
(impl/test-intake! "SLF4J -> Telemere" #(.info sl %)))] (impl/test-interop! "SLF4J -> Telemere" #(.info sl %)))]
{:present? true {:present? true
:telemere-provider-present? true
:sending->telemere? sending? :sending->telemere? sending?
:telemere-receiving? receiving?})) :telemere-receiving? receiving?}))
(impl/add-intake-check! :slf4j check-intake) (impl/add-interop-check! :slf4j check-interop)
(impl/on-init (impl/on-init
(impl/signal! (impl/signal!
{:kind :event {:kind :event
:level :debug ; < :info since runs on init :level :debug ; < :info since runs on init
:id :taoensso.telemere/slf4j->telemere! :id :taoensso.telemere/slf4j->telemere!
:msg "Enabling intake: SLF4J -> Telemere"})) :msg "Enabling interop: SLF4J -> Telemere"}))

View file

@ -1,368 +0,0 @@
(ns taoensso.telemere
"Structured telemetry for Clojure/Script applications.
See the GitHub page (esp. Wiki) for info on motivation and design:
<https://www.taoensso.com/telemere>"
{:author "Peter Taoussanis (@ptaoussanis)"}
(:refer-clojure :exclude [binding newline])
(:require
[taoensso.encore :as enc :refer [binding have have?]]
[taoensso.encore.signals :as sigs]
[taoensso.telemere.impl :as impl]
[taoensso.telemere.utils :as utils]
#?(:default [taoensso.telemere.consoles :as consoles])
#?(:clj [taoensso.telemere.streams :as streams])
#?(:clj [taoensso.telemere.files :as files]))
#?(:cljs
(:require-macros
[taoensso.telemere :refer
[with-signal with-signals
signal! event! log! trace! spy! catch->error!
;; Via `sigs/def-api`
without-filters with-kind-filter with-ns-filter with-id-filter
with-min-level with-handler with-handler+
with-ctx with-ctx+ with-middleware]])))
(comment
(remove-ns 'taoensso.telemere)
(:api (enc/interns-overview)))
(enc/assert-min-encore-version [3 115 0])
;;;; TODO
;; - Solution and docs for lib authors
;; - Add handlers: Logstash, Carmine, Datadog, Kafka
;; - Update Tufte (signal API, config API, signal keys, etc.)
;; - Update Timbre (signal API, config API, signal keys, backport improvements)
;;;; Shared signal API
(sigs/def-api
{:sf-arity 4
:ct-sig-filter impl/ct-sig-filter
:*rt-sig-filter* impl/*rt-sig-filter*
:*sig-handlers* impl/*sig-handlers*})
;;;; Aliases
(enc/defaliases
;; Encore
#?(:clj enc/set-var-root!)
#?(:clj enc/update-var-root!)
#?(:clj enc/get-env)
#?(:clj enc/call-on-shutdown!)
enc/chance
enc/rate-limiter
enc/newline
enc/comp-middleware
sigs/default-handler-dispatch-opts
;; Impl
impl/msg-splice
impl/msg-skip
#?(:clj impl/with-signal)
#?(:clj impl/with-signals)
#?(:clj impl/signal!)
;; Utils
utils/format-signal-fn
utils/pr-signal-fn
utils/error-signal?)
;;;; Help
(do
(impl/defhelp help:signal-creators :signal-creators)
(impl/defhelp help:signal-options :signal-options)
(impl/defhelp help:signal-content :signal-content)
(impl/defhelp help:environmental-config :environmental-config))
;;;;
(def ^:dynamic *uid-fn*
"Experimental, subject to change.
(fn [root?]) used to generate signal `:uid` values when tracing.
These are basically unique signal instance identifiers.
`root?` argument is true iff signal is a top-level trace (i.e. form
being traced is unnested = has no parent form).
Root uids typically have ~128 bits of entropy to ensure uniqueness.
Child uids are typically used only with respect to a parent/root,
and so can often make do with ~64 bits of entropy or less.
Smaller uids are generally cheaper to generate, and use less space
when serializing/transmitting/storing/etc.
By default generates nano-style uids like
\"r76-B8LoIPs5lBG1_Uhdy\" (root) and \"tMEYoZH0K-\" (non-root).
For plain fixed-length UUIDs use: (fn [_root?] (utils/uuid))
For plain fixed-length UUID strings use: (fn [_root?] (utils/uuid-str))
See also `utils/nano-uid-fn`, `utils/hex-id-fn`, etc."
(utils/nano-uid-fn {:secure? false}))
(comment (enc/qb 1e6 (enc/uuid) (*uid-fn* true) (*uid-fn* false))) ; [168.83 79.02 62.95]
;;;; Signal creators
;; - event! [id ] [id opts/level] ; id + ?level => allowed? ; Sole signal with descending main arg!
;; - log! [msg ] [opts/level msg] ; msg + ?level => allowed?
;; - error! [error] [opts/id error] ; error + ?id => given error
;; - trace! [form ] [opts/id form] ; run + ?id => run result (value or throw)
;; - spy! [form ] [opts/level form] ; run + ?level => run result (value or throw)
;; - catch->error! [form ] [opts/id form] ; run + ?id => run value or ?return
;; - signal! [opts ] ; => allowed? / run result (value or throw)
;; - uncaught->error! [opts/id] ; ?id => nil
#?(:clj
(defmacro event!
"[id] [id level-or-opts] => allowed?"
{:doc (impl/signal-docstring :event!)
:arglists (impl/signal-arglists :event!)}
[& args]
(let [opts (impl/signal-opts `event! {:kind :event, :level :info} :id :level :dsc args)]
(enc/keep-callsite `(impl/signal! ~opts)))))
(comment (with-signal (event! ::my-id :info)))
#?(:clj
(defmacro log!
"[msg] [level-or-opts msg] => allowed?"
{:doc (impl/signal-docstring :log!)
:arglists (impl/signal-arglists :log!)}
[& args]
(let [opts (impl/signal-opts `log! {:kind :log, :level :info} :msg :level :asc args)]
(enc/keep-callsite `(impl/signal! ~opts)))))
(comment (with-signal (log! :info "My msg")))
#?(:clj
(defmacro error!
"[error] [error id-or-opts] => error"
{:doc (impl/signal-docstring :error!)
:arglists (impl/signal-arglists :error!)}
[& args]
(let [opts (impl/signal-opts `error! {:kind :error, :level :error} :error :id :asc args)
error-form (get opts :error)]
(enc/keep-callsite
`(let [~'__error ~error-form]
(impl/signal! ~(assoc opts :error '__error))
~'__error ; Unconditional!
)))))
(comment (with-signal (throw (error! ::my-id (ex-info "MyEx" {})))))
#?(:clj
(defmacro catch->error!
"[form] [id-or-opts form] => run value or ?catch-val"
{:doc (impl/signal-docstring :catch-to-error!)
:arglists (impl/signal-arglists :catch->error!)}
[& args]
(let [opts (impl/signal-opts `catch->error! {:kind :error, :level :error} ::__form :id :asc args)
rethrow? (if (contains? opts :catch-val) false (get opts :rethrow? true))
catch-val (get opts :catch-val)
catch-sym (get opts :catch-sym '__caught-error) ; Undocumented
form (get opts ::__form)
opts (dissoc opts ::__form :catch-val :catch-sym :rethrow?)]
(enc/keep-callsite
`(enc/try* ~form
(catch :all ~catch-sym
(impl/signal! ~(assoc opts :error catch-sym))
(if ~rethrow? (throw ~catch-sym) ~catch-val)))))))
(comment
(with-signal (catch->error! ::my-id (/ 1 0)))
(with-signal (catch->error! { :msg ["Error:" __caught-error]} (/ 1 0)))
(with-signal (catch->error! {:catch-sym my-err :msg ["Error:" my-err]} (/ 1 0))))
#?(:clj
(defmacro trace!
"[form] [id-or-opts form] => run result (value or throw)"
{:doc (impl/signal-docstring :trace!)
:arglists (impl/signal-arglists :trace!)}
[& args]
(let [opts
(impl/signal-opts `trace!
{:location (enc/get-source &form &env) ; For catch-opts
:kind :trace, :level :info, :msg `impl/default-trace-msg}
:run :id :asc args)
;; :catch->error <id-or-opts> currently undocumented
[opts catch-opts] (impl/signal-catch-opts opts)]
(if catch-opts
(enc/keep-callsite `(catch->error! ~catch-opts (impl/signal! ~opts)))
(enc/keep-callsite `(impl/signal! ~opts))))))
(comment
(with-signal (trace! ::my-id (+ 1 2)))
(let [[_ [s1 s2]]
(with-signals
(trace! {:id :id1, :catch->error :id2}
(throw (ex-info "Ex1" {}))))]
[s2]))
#?(:clj
(defmacro spy!
"[form] [level-or-opts form] => run result (value or throw)"
{:doc (impl/signal-docstring :spy!)
:arglists (impl/signal-arglists :spy!)}
[& args]
(let [opts
(impl/signal-opts `spy!
{:location (enc/get-source &form &env) ; For catch-opts
:kind :spy, :level :info, :msg `impl/default-trace-msg}
:run :level :asc args)
;; :catch->error <id-or-opts> currently undocumented
[opts catch-opts] (impl/signal-catch-opts opts)]
(if catch-opts
(enc/keep-callsite `(catch->error! ~catch-opts (impl/signal! ~opts)))
(enc/keep-callsite `(impl/signal! ~opts))))))
(comment (with-signal :force (spy! :info (+ 1 2))))
#?(:clj
(defmacro uncaught->error!
"Uses `uncaught->handler!` so that `error!` will be called for
uncaught JVM errors.
See `uncaught->handler!` and `error!` for details."
{:arglists (impl/signal-arglists :uncaught->error!)}
[& args]
(let [msg-form ["Uncaught Throwable on thread: " `(.getName ~(with-meta '__thread {:tag 'java.lang.Thread}))]
opts
(impl/signal-opts `uncaught->error!
{:kind :error, :level :error, :msg msg-form}
:error :id :dsc (into ['__throwable] args))]
(enc/keep-callsite
`(uncaught->handler!
(fn [~'__thread ~'__throwable]
(impl/signal! ~opts)))))))
(comment (macroexpand '(uncaught->error! ::my-id)))
#?(:clj
(defn uncaught->handler!
"Sets JVM's global `DefaultUncaughtExceptionHandler` to given
(fn handler [`<java.lang.Thread>` `<java.lang.Throwable>`]).
See also `uncaught->error!`."
[handler]
(Thread/setDefaultUncaughtExceptionHandler
(reify Thread$UncaughtExceptionHandler
(uncaughtException [_ thread throwable]
(handler thread throwable))))
nil))
;;;; Intake
#?(:clj
(enc/defaliases
impl/check-intakes
streams/with-out->telemere
streams/with-err->telemere
streams/with-streams->telemere
streams/streams->telemere!
streams/streams->reset!))
(comment (check-intakes))
;;;; Handlers
(enc/defaliases
#?(:default consoles/handler:console)
#?(:cljs consoles/handler:console-raw)
#?(:clj files/handler:file))
;;;; Init
(impl/on-init
(enc/set-var-root! sigs/*default-handler-error-fn*
(fn [{:keys [error] :as m}]
(impl/signal!
{:kind :error
:level :error
:error error
:location {:ns "taoensso.encore.signals"}
:id :taoensso.encore.signals/handler-error
:msg "Error executing wrapped handler fn"
:data (dissoc m :error)})))
(enc/set-var-root! sigs/*default-handler-backp-fn*
(fn [data]
(impl/signal!
{:kind :event
:level :warn
:location {:ns "taoensso.encore.signals"}
:id :taoensso.encore.signals/handler-back-pressure
:msg "Back pressure on wrapped handler fn"
:data data})))
(add-handler! :default/console (handler:console))
#?(:clj (enc/catching (require '[taoensso.telemere.tools-logging])))
#?(:clj (enc/catching (require '[taoensso.telemere.slf4j]))))
;;;; Flow benchmarks
(comment
{:last-updated "2024-08-15"
:system "2020 Macbook Pro M1, 16 GB memory"
:clojure-version "1.12.0-rc1"
:java-version "OpenJDK 22"}
[(binding [impl/*sig-handlers* nil]
(enc/qb 1e6 ; [9.31 16.76 264.12 350.43]
(signal! {:level :info, :run nil, :elide? true }) ; 9
(signal! {:level :info, :run nil, :allow? false}) ; 17
(signal! {:level :info, :run nil, :allow? true }) ; 264
(signal! {:level :info, :run nil }) ; 350
))
(binding [impl/*sig-handlers* nil]
(enc/qb 1e6 ; [8.34 15.78 999.27 444.08 1078.83]
(signal! {:level :info, :run "run", :elide? true }) ; 8
(signal! {:level :info, :run "run", :allow? false}) ; 16
(signal! {:level :info, :run "run", :allow? true }) ; 1000
(signal! {:level :info, :run "run", :trace? false}) ; 444
(signal! {:level :info, :run "run" }) ; 1079
))
;; For README "performance" table
(binding [impl/*sig-handlers* nil]
(enc/qb [8 1e6] ; [9.34 347.7 447.71 1086.65]
(signal! {:level :info, :elide? true }) ; 9
(signal! {:level :info }) ; 348
(signal! {:level :info, :run "run", :trace? false}) ; 448
(signal! {:level :info, :run "run" }) ; 1087
))])
;;;;
(comment
(with-handler :hid1 (handler:console) {} (log! "Message"))
(let [sig
(with-signal
(event! ::ev-id
{:data {:a :A :b :b}
:error
(ex-info "Ex2" {:b :B}
(ex-info "Ex1" {:a :A}))}))]
(do (let [hf (handler:file)] (hf sig) (hf)))
(do (let [hf (handler:console)] (hf sig) (hf)))
#?(:cljs (let [hf (handler:console-raw)] (hf sig) (hf)))))
(comment (let [[_ [s1 s2]] (with-signals (trace! ::id1 (trace! ::id2 "form2")))] s1))

View file

@ -1,695 +0,0 @@
(ns ^:no-doc taoensso.telemere.impl
"Private ns, implementation detail.
Signal design shared by: Telemere, Tufte, Timbre."
(:refer-clojure :exclude [binding])
(:require
[taoensso.encore :as enc :refer [binding have have?]]
[taoensso.encore.signals :as sigs])
#?(:cljs
(:require-macros
[taoensso.telemere.impl :refer [with-signal]])))
(comment
(remove-ns 'taoensso.telemere.impl)
(:api (enc/interns-overview)))
#?(:clj
(enc/declare-remote ; For macro expansions
^:dynamic taoensso.telemere/*ctx*
^:dynamic taoensso.telemere/*middleware*
^:dynamic taoensso.telemere/*uid-fn*))
;;;; Utils
#?(:clj
(defmacro on-init [& body]
(let [sym (with-meta '__on-init {:private true})
compiling? (if (:ns &env) false `*compile-files*)]
`(defonce ~sym (when-not ~compiling? ~@body nil)))))
(comment (macroexpand-1 '(on-init (println "foo"))))
;;;; Config
#?(:clj
(let [base (enc/get-env {:as :edn} :taoensso.telemere/ct-filters<.platform><.edn>)
kind-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-kind-filter<.platform><.edn>)
ns-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-ns-filter<.platform><.edn>)
id-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-id-filter<.platform><.edn>)
min-level (enc/get-env {:as :edn} :taoensso.telemere/ct-min-level<.platform><.edn>)]
(enc/defonce ct-sig-filter
"`SigFilter` used for compile-time elision, or nil."
(sigs/sig-filter
{:kind-filter (or kind-filter (get base :kind-filter))
:ns-filter (or ns-filter (get base :ns-filter))
:id-filter (or id-filter (get base :id-filter))
:min-level (or min-level (get base :min-level))}))))
(let [base (enc/get-env {:as :edn} :taoensso.telemere/rt-filters<.platform><.edn>)
kind-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-kind-filter<.platform><.edn>)
ns-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-ns-filter<.platform><.edn>)
id-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-id-filter<.platform><.edn>)
min-level (enc/get-env {:as :edn, :default :info} :taoensso.telemere/rt-min-level<.platform><.edn>)]
(enc/defonce ^:dynamic *rt-sig-filter*
"`SigFilter` used for runtime filtering, or nil."
(sigs/sig-filter
{:kind-filter (or kind-filter (get base :kind-filter))
:ns-filter (or ns-filter (get base :ns-filter))
:id-filter (or id-filter (get base :id-filter))
:min-level (or min-level (get base :min-level))})))
(comment (enc/get-env {:as :edn, :return :explain} :taoensso.telemere/rt-filters<.platform><.edn>))
;;;; Messages
(deftype MsgSkip [])
(deftype MsgSplice [args])
(def ^:public msg-skip
"For use within signal message vectors.
Special value that will be ignored (noop) when creating message.
Useful for conditionally skipping parts of message content, etc.:
(signal! {:msg [\"Hello\" (if <cond> <then> msg-skip) \"world\"] <...>}) or
(log! [\"Hello\" (if <cond> <then> msg-skip) \"world\"]), etc.
%> {:msg_ \"Hello world\" <...>}"
(MsgSkip.))
(defn ^:public msg-splice
"For use within signal message vectors.
Wraps given arguments so that they're spliced when creating message.
Useful for conditionally splicing in extra message content, etc.:
(signal! {:msg [(when <cond> (msg-splice [\"Username:\" \"Steve\"])) <...>]}) or
(log! [(when <cond> (msg-splice [\"Username:\" \"Steve\"]))])
%> {:msg_ \"Username: Steve\"}"
[args] (MsgSplice. args))
(let [;; xform (map #(if (nil? %) "nil" %))
xform
(fn [rf]
(let [;; Protocol-based impln (extensible but ~20% slower)
;; rf* (fn rf* [acc in] (reduce-msg-arg in acc rf))
rf*
(fn rf* [acc in]
(enc/cond
(instance? MsgSplice in) (reduce rf* acc (.-args ^MsgSplice in))
(instance? MsgSkip in) acc
(nil? in) (rf acc "nil")
:else (rf acc in)))]
(fn
([ ] (rf))
([acc ] (rf acc))
([acc in] (rf* acc in)))))]
(defn signal-msg
"Returns string formed by joining all args with \" \" separator,
rendering nils as \"nil\". Supports `msg-skip`, `msg-splice`.
API intended to be usefully different to `str`:
- `str`: no spacers, skip nils, no splicing
- `signal-msg`: auto spacers, show nils, opt-in splicing"
{:tag #?(:clj 'String :cljs 'string)}
[args] (enc/str-join " " xform args)))
(comment
(enc/qb 2e6 ; [305.61 625.35]
(str "a" "b" "c" nil :kw) ; "abc:kw"
(signal-msg ["a" "b" "c" nil :kw (msg-splice ["d" "e"])]) ; "a b c nil :kw d e"
))
#?(:clj
(defn- parse-msg-form [msg-form]
(when msg-form
(enc/cond
(string? msg-form) msg-form
(vector? msg-form)
(enc/cond
(empty? msg-form) nil
:let [[m1 & more] msg-form]
(and (string? m1) (nil? more)) m1
:else `(delay (signal-msg ~msg-form)))
;; Auto delay-wrap (user should never delay-wrap!)
;; :else `(delay ~msg-form)
;; Leave user to delay-wrap when appropriate (document)
:else msg-form))))
(defn default-trace-msg
[form value error nsecs]
(if error
(str form " !> " (enc/ex-type error))
(str form " => " value)))
(comment
(default-trace-msg "(+ 1 2)" 3 nil 12345)
(default-trace-msg "(+ 1 2)" nil (Exception. "Ex") 12345))
;;;; Tracing (optional flow tracking)
(enc/def* ^:dynamic *trace-root* "?{:keys [id uid inst]}" nil)
(enc/def* ^:dynamic *trace-parent* "?{:keys [id uid inst]}" nil)
#?(:clj
(defmacro cond-binding
"Wraps `form` with binding if `bind?` is true."
[bind? bindings body-form]
(if bind?
`(binding ~bindings ~body-form)
(do body-form))))
(comment
[(enc/qb 1e6 (cond-binding true [*trace-parent* {:id :id1, :uid :uid1, :inst :inst1}] *trace-parent*)) ; 226.18
(macroexpand '(cond-binding true [*trace-parent* {:id :id1, :uid :uid1, :inst :inst1}] *trace-parent*))])
#?(:clj
(enc/compile-if io.opentelemetry.context.Context/current
(defmacro otel-context [] `(io.opentelemetry.context.Context/current))
(defmacro otel-context [] nil)))
(comment (enc/qb 1e6 (otel-context))) ; 20.43
;;;; Main types
(defrecord Signal
;; Telemere's main public data type, we avoid nesting and duplication
[^long schema inst uid,
location ns line column file, #?@(:clj [host thread _otel-context]),
sample-rate, kind id level, ctx parent root, data kvs msg_,
error run-form run-val end-inst run-nsecs]
Object (toString [sig] (str "#" `Signal (into {} sig))))
(do (enc/def-print-impl [sig Signal] (str "#" `Signal (pr-str (into {} sig)))))
#?(:clj (enc/def-print-dup [sig Signal] (str "#" `Signal (pr-str (into {} sig))))) ; NB intentionally verbose, to support extra keys
(def standard-signal-keys
(disj (set (keys (map->Signal {:schema 0})))
:_otel-context))
(comment
(def s1 (with-signal (signal! {:level :info, :my-k1 :my-v1})))
(read-string (str (assoc s1 :my-k2 :my-v2)))
(read-string (pr-str (assoc s1 :my-k2 :my-v2)))
(read-string (binding [*print-dup* true] (pr-str (assoc s1 :my-k2 :my-v2))))
(defrecord MyRec [x])
(read-string ; Non-verbose will fail on any extra keys
(binding [*print-dup* true, *verbose-defrecords* false]
(pr-str (assoc (MyRec. :x) :y :y)))))
(deftype #_defrecord WrappedSignal
;; Internal type to implement `sigs/IFilterableSignal`,
;; incl. lazy + cached `signal-value_` field.
[ns kind id level signal-value_]
sigs/IFilterableSignal
(allow-signal? [_ sig-filter] (sig-filter kind ns id level))
(signal-value [_ handler-sample-rate]
(let [sig-val @signal-value_]
(or
(when handler-sample-rate
(when (map? sig-val)
;; Replace call sample rate with combined (call * handler) sample rate
(assoc sig-val :sample-rate
(*
(double handler-sample-rate)
(double (or (get sig-val :sample-rate) 1.0))))))
sig-val))))
;;;; Handlers
(enc/defonce ^:dynamic *sig-spy* "To support `with-signals`, etc." nil)
(enc/defonce ^:dynamic *sig-handlers* "?[<wrapped-handler-fn>]" nil)
(defn force-msg-in-sig [sig]
(if-not (map? sig)
sig
(if-let [e (find sig :msg_)]
(assoc sig :msg_ (force (val e)))
(do sig))))
#?(:clj
(defmacro ^:public with-signal
"Experimental, subject to change.
Executes given form, trapping errors. Returns the LAST signal created by form.
Useful for tests/debugging.
Options:
`trap-signals?` (default false)
Should ALL signals created by form be trapped to prevent normal dispatch
to registered handlers?
`raw-msg?` (default false)
Should delayed `:msg_` in returned signal be retained as-is?
Delay is otherwise replaced by realized string.
See also `with-signals`."
([ form] `(with-signal false false ~form))
([ trap-signals? form] `(with-signal false ~trap-signals? ~form))
([raw-msg? trap-signals? form]
`(let [sig_# (volatile! nil)]
(binding [*sig-spy* [sig_# :last-only ~trap-signals?]]
(enc/try* ~form (catch :all _#)))
(if ~raw-msg?
(do @sig_#)
(force-msg-in-sig @sig_#))))))
#?(:clj
(defmacro ^:public with-signals
"Experimental, subject to change.
Like `with-signal` but returns [[<form-value> <form-error>] [<signal1> ...]].
Useful for tests/debugging."
([ form] `(with-signals false false ~form))
([ trap-signals? form] `(with-signals false ~trap-signals? ~form))
([raw-msgs? trap-signals? form]
`(let [sigs_# (volatile! nil)
form-result#
(binding [*sig-spy* [sigs_# (not :last-only) ~trap-signals?]]
(enc/try*
(do [~form nil])
(catch :all t# [nil t#])))
sigs#
(if ~raw-msgs?
(do @sigs_#)
(mapv force-msg-in-sig @sigs_#))]
[form-result# (not-empty sigs#)]))))
(defn dispatch-signal!
"Dispatches given signal to registered handlers, supports `with-signal/s`."
[signal]
(or
(when-let [[v_ last-only? trap-signals?] *sig-spy*]
(let [sv (sigs/signal-value signal nil)]
(if last-only?
(vreset! v_ sv)
(vswap! v_ #(conj (or % []) sv))))
(when trap-signals? :stop))
(sigs/call-handlers! *sig-handlers* signal)))
;;;; Signal API helpers
#?(:clj (defmacro signal-docstring [ rname] (enc/slurp-resource (str "signal-docstrings/" (name rname) ".txt"))))
#?(:clj (defmacro defhelp [sym rname] `(enc/def* ~sym {:doc ~(eval `(signal-docstring ~rname))} "See docstring")))
#?(:clj
(defn signal-arglists [macro-id]
(case macro-id
:signal! ; [opts] => allowed? / run result (value or throw)
'([{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id, ; Undocumented
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error run & kvs]}])
:event! ; [id] [id level-or-opts] => allowed?
'([id ]
[id level]
[id
{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id,
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error #_run & kvs]}])
:log! ; [msg] [level-or-opts msg] => allowed?
'([ msg]
[level msg]
[{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id,
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error #_run & kvs]}
msg])
:error! ; [error] [id-or-opts error] => given error
'([ error]
[id error]
[{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id,
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error #_run & kvs]}
error])
(:trace! :spy!) ; [form] [id-or-opts form] => run result (value or throw)
'([ form]
[id form]
[{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id,
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error run & kvs]}
form])
:catch->error! ; [form] [id-or-opts form] => run result (value or throw)
'([ form]
[id form]
[{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id, rethrow? catch-val,
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error #_run & kvs]}
form])
:uncaught->error! ; [] [id-or-opts] => nil
'([ ]
[id]
[{:as opts :keys
[#_defaults #_elide? #_allow? #_expansion-id,
elidable? location inst uid middleware,
sample-rate kind ns id level when rate-limit,
ctx parent root trace?, do let data msg error #_run & kvs]}])
(enc/unexpected-arg! macro-id))))
#?(:clj
(defn signal-opts
"Util to help write common signal wrapper macros."
[context defaults main-key extra-key arg-order args]
(enc/cond
:let [context-name (str "`" (name context) "`")
num-args (count args)
bad-args!
(fn [msg data]
(throw
(ex-info (str "Invalid " context-name " args: " msg)
(conj
{:context context
:args args}
data))))]
(not (#{1 2} num-args))
(bad-args! (str "wrong number of args (" num-args ")")
{:actual num-args, :expected #{1 2}})
:let [[main-arg extra-arg]
(case arg-order
:dsc args ; [main ...]
:asc (reverse args) ; [... main]
(enc/unexpected-arg!
arg-order))
extra-arg? (= num-args 2)
extra-opts? (and extra-arg? (map? extra-arg))]
:do
(cond
(map? main-arg)
(bad-args! "single map arg is USUALLY a mistake, so isn't allowed. Please use 2 arg call if this is intentional." {})
(and extra-opts? (contains? extra-arg main-key))
(bad-args! (str "given opts should not contain `" main-key "`.") {}))
extra-opts? (merge defaults {main-key main-arg} extra-arg)
extra-arg? (merge defaults {main-key main-arg, extra-key extra-arg})
:else (merge defaults {main-key main-arg}))))
(comment (signal-opts `foo! {:level :info} :id :level :dsc [::my-id {:level :warn}]))
#?(:clj
(defn signal-catch-opts
"For use within `trace!` and `spy!`, etc."
[main-opts]
(let [catch-id-or-opts (get main-opts :catch->error)
main-opts (dissoc main-opts :catch->error)
catch-opts
(when catch-id-or-opts
(let [base ; Inherit some opts from main
(enc/assoc-some {}
:location (get main-opts :location)
:id (get main-opts :id))]
(cond
(true? catch-id-or-opts) (do base)
(map? catch-id-or-opts) (conj base catch-id-or-opts)
:else (conj base {:id catch-id-or-opts}))))]
[main-opts catch-opts])))
(comment
(signal-catch-opts {:id :main-id, :catch->error true})
(signal-catch-opts {:id :main-id, :catch->error :error-id})
(signal-catch-opts {:id :main-id, :catch->error {:id :error-id}}))
;;;; Signal macro
(deftype RunResult [value error ^long run-nsecs]
#?(:clj clojure.lang.IFn :cljs IFn)
(#?(:clj invoke :cljs -invoke) [_] (if error (throw error) value))
(#?(:clj invoke :cljs -invoke) [_ signal_]
(if error
(throw
(ex-info "Signal `:run` form error"
(enc/try*
(do {:taoensso.telemere/signal (force signal_)})
(catch :all t {:taoensso.telemere/signal-error t}))
error))
value)))
(defn inst+nsecs
"Returns given platform instant plus given number of nanosecs."
[inst run-nsecs]
#?(:clj (.plusNanos ^java.time.Instant inst run-nsecs)
:cljs (js/Date. (+ (.getTime inst) (/ run-nsecs 1e6)))))
(comment (enc/qb 1e6 (inst+nsecs (enc/now-inst) 1e9)))
#?(:clj
(defmacro ^:public signal!
"Generic low-level signal call, also aliased in Encore."
{:doc (signal-docstring :signal!)
:arglists (signal-arglists :signal!)}
[opts]
(have? map? opts) ; We require const map keys, but vals may require eval
(let [defaults (get opts :defaults)
opts (merge defaults (dissoc opts :defaults))
clj? (not (:ns &env))
{run-form :run} opts
{:keys [#_expansion-id location elide? allow?]}
(sigs/filterable-expansion
{:macro-form &form
:macro-env &env
:sf-arity 4
:ct-sig-filter ct-sig-filter
:*rt-sig-filter* `*rt-sig-filter*}
opts)]
(if elide?
run-form
(let [{ns-form :ns
line-form :line
column-form :column
file-form :file} location
{inst-form :inst
level-form :level
kind-form :kind
id-form :id} opts
trace? (get opts :trace? (boolean run-form))
_
(when-not (contains? #{true false nil} trace?)
(enc/unexpected-arg! trace?
{:msg "Expected constant (compile-time) `:trace?` boolean"
:context `signal!}))
parent-form (get opts :parent (when trace? `taoensso.telemere.impl/*trace-parent*))
root-form (get opts :root (when trace? `taoensso.telemere.impl/*trace-root*))
inst-form (get opts :inst :auto)
uid-form (get opts :uid (when trace? :auto))
inst-form (if (not= inst-form :auto) inst-form `(enc/now-inst*))
uid-form (if (not= uid-form :auto) uid-form `(taoensso.telemere/*uid-fn* (if ~'__root0 false true)))
thread-form (if clj? `(enc/thread-info) nil)
signal-delay-form
(let [{do-form :do
let-form :let
msg-form :msg
data-form :data
error-form :error
sample-rate-form :sample-rate} opts
let-form (or let-form '[])
msg-form (parse-msg-form msg-form)
ctx-form (get opts :ctx `taoensso.telemere/*ctx*)
middleware-form (get opts :middleware `taoensso.telemere/*middleware*)
kvs-form
(not-empty
(dissoc opts
:elidable? :location :inst :uid :middleware,
:sample-rate :ns :kind :id :level :filter :when #_:rate-limit,
:ctx :parent #_:trace?, :do :let :data :msg :error :run
:elide? :allow? #_:expansion-id))
_ ; Compile-time validation
(do
(when (and run-form error-form) ; Ambiguous source of error
(throw
(ex-info "Signals cannot have both `:run` and `:error` opts at the same time"
{:run-form run-form
:error-form error-form
:location location
:other-opts (dissoc opts :run :error)})))
(when-let [e (find opts :msg_)] ; Common typo/confusion
(throw
(ex-info "Signals cannot have `:msg_` opt (did you mean `:msg`?))"
{:msg_ (enc/typed-val (val e))}))))
signal-form
(let [record-form
(let [clause [(if run-form :run :no-run) (if clj? :clj :cljs)]]
(case clause
[:run :clj ] `(Signal. 1 ~'__inst ~'__uid, ~location ~'__ns ~line-form ~column-form ~file-form, (enc/host-info) ~'__thread (otel-context), ~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root, ~data-form ~kvs-form ~'_msg_, ~'_run-err '~run-form ~'_run-val ~'_end-inst ~'_run-nsecs)
[:run :cljs] `(Signal. 1 ~'__inst ~'__uid, ~location ~'__ns ~line-form ~column-form ~file-form, ~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root, ~data-form ~kvs-form ~'_msg_, ~'_run-err '~run-form ~'_run-val ~'_end-inst ~'_run-nsecs)
[:no-run :clj ] `(Signal. 1 ~'__inst ~'__uid, ~location ~'__ns ~line-form ~column-form ~file-form, (enc/host-info) ~'__thread (otel-context), ~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root, ~data-form ~kvs-form ~msg-form, ~error-form nil nil nil nil)
[:no-run :cljs] `(Signal. 1 ~'__inst ~'__uid, ~location ~'__ns ~line-form ~column-form ~file-form, ~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root, ~data-form ~kvs-form ~msg-form, ~error-form nil nil nil nil)
(enc/unexpected-arg! clause {:context :signal-constructor-args})))
record-form
(if-not run-form
record-form
`(let [~(with-meta '_run-result {:tag `RunResult}) ~'__run-result
~'_run-nsecs (.-run-nsecs ~'_run-result)
~'_run-val (.-value ~'_run-result)
~'_run-err (.-error ~'_run-result)
~'_end-inst (inst+nsecs ~'__inst ~'_run-nsecs)
~'_msg_
(let [mf# ~msg-form]
(if (fn? mf#) ; Undocumented, handy for `trace!`/`spy!`, etc.
(delay (mf# '~run-form ~'_run-val ~'_run-err ~'_run-nsecs))
mf#))]
~record-form))]
(if-not kvs-form
record-form
`(let [signal# ~record-form]
(reduce-kv assoc signal# (.-kvs signal#)))))]
`(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, eval BEFORE data, msg, etc.
signal# ~signal-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 (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 [;;; Allow to throw at call
~'__inst ~inst-form
~'__root0 ~root-form
~'__level ~level-form
~'__kind ~kind-form
~'__id ~id-form
~'__uid ~uid-form
~'__ns ~ns-form
~'__thread ~thread-form
~'__root (or ~'__root0 (when ~trace? {:id ~'__id, :uid ~'__uid, :inst ~'__inst}))
~'__run-result ; Non-throwing (traps)
~(when run-form
`(let [t0# (enc/now-nano*)]
(cond-binding ~trace?
[*trace-root* ~'__root
*trace-parent* {:id ~'__id, :uid ~'__uid, :inst ~'__inst}]
(enc/try*
(do (RunResult. ~run-form nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#)))))))
signal_# ~signal-delay-form]
(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_#))
true))))))))
(comment
(with-signal (signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)}))
(macroexpand '(signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)}))
(do
(println "---")
(sigs/with-handler *sig-handlers* "hf1" (fn hf1 [x] (println x)) {}
(signal! {:level :info, :run "run"}))))
#?(:clj
(defmacro signal-allowed?
"Used only for intake (SLF4J, `tools.logging`, etc.)."
{:arglists (signal-arglists :signal!)}
[opts]
(let [{:keys [#_expansion-id #_location elide? allow?]}
(sigs/filterable-expansion
{:macro-form &form
:macro-env &env
:sf-arity 4
:ct-sig-filter ct-sig-filter
:*rt-sig-filter* `*rt-sig-filter*}
opts)]
(and (not elide?) allow?))))
;;;; Intakes
#?(:clj
(do
(enc/defonce ^:private intake-checks_
"{<source-id> (fn check [])}"
(atom
{:tools-logging (fn [] {:present? (enc/have-resource? "clojure/tools/logging.clj")})
:slf4j (fn [] {:present? (enc/compile-when org.slf4j.Logger true false)})}))
(defn add-intake-check! [source-id check-fn] (swap! intake-checks_ assoc source-id check-fn))
(defn ^:public check-intakes
"Experimental, subject to change.
Runs Telemere's registered intake checks and returns
{<source-id> {:keys [sending->telemere? telemere-receiving? ...]}}.
Useful for tests/debugging."
[]
(enc/map-vals (fn [check-fn] (check-fn))
@intake-checks_))
(defn test-intake! [msg test-fn]
(let [msg (str "Intake test: " msg " (" (enc/uuid-str) ")")
signal
(binding [*rt-sig-filter* nil] ; Without runtime filters
(with-signal :raw :trap (test-fn msg)))]
(= (force (get signal :msg_)) msg)))))

View file

@ -1,639 +0,0 @@
(ns taoensso.telemere.open-telemetry
"OpenTelemetry handler using `opentelemetry-java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>,
<https://javadoc.io/doc/io.opentelemetry/opentelemetry-api/latest/index.html>"
(:require
[clojure.string :as str]
[clojure.set :as set]
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.utils :as utils]
[taoensso.telemere.impl :as impl]
[taoensso.telemere :as tel])
(:import
[io.opentelemetry.context Context]
[io.opentelemetry.api.common AttributesBuilder Attributes]
[io.opentelemetry.api.logs LoggerProvider Severity]
[io.opentelemetry.api.trace TracerProvider Tracer Span]
[java.util.concurrent CountDownLatch]))
(comment
(remove-ns 'taoensso.telemere.open-telemetry)
(:api (enc/interns-overview)))
;;;; TODO
;; - API for `remote-span-context`, trace state, span links?
;; - Ability to actually set (compatible) traceId, spanId?
;;;; Providers
(defn get-default-providers
"Experimental, subject to change. Feedback welcome!
Returns map with keys:
:logger-provider - default `io.opentelemetry.api.logs.LoggerProvider`
:tracer-provider - default `io.opentelemetry.api.trace.TracerProvider`
:via - #{:sdk-extension-autoconfigure :global}
Uses `AutoConfiguredOpenTelemetrySdk` when possible, or
`GlobalOpenTelemetry` otherwise.
See the relevant `opentelemetry-java` docs for details."
[]
(or
;; Via SDK autoconfiguration extension (when available)
(enc/compile-when
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
(enc/catching :common
(let [builder (io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk/builder)
sdk (.getOpenTelemetrySdk (.build builder))]
{:logger-provider (.getLogsBridge sdk)
:tracer-provider (.getTracerProvider sdk)
:via :sdk-extension-autoconfigure})))
;; Via Global (generally not recommended)
(let [g (io.opentelemetry.api.GlobalOpenTelemetry/get)]
{:logger-provider (.getLogsBridge g)
:tracer-provider (.getTracerProvider g)
:via :global})))
(def ^:no-doc default-providers_
(delay (get-default-providers)))
(comment
(get-default-providers)
(let [{:keys [logger-provider tracer-provider]} (get-default-providers)]
(def ^LoggerProvider my-lp logger-provider)
(def ^Tracer my-tr (.get tracer-provider "Telemere")))
;; Confirm that we have a real (not noop) SpanBuilder
(.spanBuilder my-tr "my-span"))
;;;; Attributes
(def ^:private ^String attr-name
"Returns cached OpenTelemetry-style name: `:a.b/c-d` -> \"a.b.c_d\", etc.
Ref. <https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/>."
(enc/fmemoize
(fn self
([prefix x] (str (self prefix) "." (self x)))
([ x]
(if-not (enc/named? x)
(str/replace (str/lower-case (str x)) #"[-\s]" "_")
(if-let [ns (namespace x)]
(str/replace (str/lower-case (str ns "." (name x))) "-" "_")
(str/replace (str/lower-case (name x)) "-" "_")))))))
(comment (enc/qb 1e6 (attr-name :a.b/c-d) (attr-name :x.y/z :a.b/c-d))) ; [44.13 63.19]
;; AttributeTypes: String, Long, Double, Boolean, and arrays
(defprotocol ^:private IAttributesBuilder (^:private -put-attr! ^AttributesBuilder [attr-val attr-name attr-builder]))
(extend-protocol IAttributesBuilder
;; nil (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k "nil")) ; As pr-edn*
nil (-put-attr! [v ^String k ^AttributesBuilder ab] ab ) ; Noop
Boolean (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
String (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
java.util.UUID (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (str v))) ; "d4fc65a0..."
clojure.lang.Named (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (str v))) ; ":foo/bar"
Long (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
Integer (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Short (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Byte (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Double (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
Float (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (double v)))
Number (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (double v)))
clojure.lang.IPersistentCollection
(-put-attr! [v ^String k ^AttributesBuilder ab]
(when-some [v1 (if (indexed? v) (nth v 0 nil) (first v))]
(or
(cond
(string? v1) (enc/catching :common (.put ab k ^"[Ljava.lang.String;" (into-array String v)))
(int? v1) (enc/catching :common (.put ab k (long-array v)))
(float? v1) (enc/catching :common (.put ab k (double-array v)))
(boolean? v1) (enc/catching :common (.put ab k (boolean-array v))))
(when-let [^String s (enc/catching :common (enc/pr-edn* v))]
(.put ab k s))))
ab)
Object
(-put-attr! [v ^String k ^AttributesBuilder ab]
(when-let [^String s (enc/catching :common (enc/pr-edn* v))]
(.put ab k s))))
(defmacro ^:private put-attr! [attr-builder attr-name attr-val]
`(-put-attr! ~attr-val ~attr-name ~attr-builder)) ; Fix arg order
(defn- merge-attrs!
"If given a map, merges prefixed key/values (~like `into`).
Otherwise just puts single named value."
[attr-builder name-or-prefix x]
(if (map? x)
(enc/run-kv! (fn [k v] (put-attr! attr-builder (attr-name name-or-prefix k) v)) x)
(do (put-attr! attr-builder name-or-prefix x))))
;;;; Spans
(defn- remote-span-context
"Returns new remote `io.opentelemetry.api.trace.SpanContext`
for use as `start-span` parent."
^io.opentelemetry.api.trace.SpanContext
[^String trace-id ^String span-id sampled? ?trace-state]
(io.opentelemetry.api.trace.SpanContext/createFromRemoteParent
trace-id span-id
(if sampled?
(io.opentelemetry.api.trace.TraceFlags/getSampled)
(io.opentelemetry.api.trace.TraceFlags/getDefault))
(enc/if-not [trace-state ?trace-state]
(io.opentelemetry.api.trace.TraceState/getDefault)
(cond
(map? trace-state)
(let [tsb (io.opentelemetry.api.trace.TraceState/builder)]
(enc/run-kv! (fn [k v] (.put tsb k v)) trace-state) ; NB only `a-zA-Z.-_` chars allowed
(.build tsb))
(instance? io.opentelemetry.api.trace.TraceState trace-state) trace-state
:else
(enc/unexpected-arg! trace-state
:context `remote-span-context
:param 'trace-state
:expected '#{nil {string string} io.opentelemetry.api.trace.TraceState})))))
(comment (enc/qb 1e6 (remote-span-context "c5b856d919f65e39a202bfb3034d65d8" "9740419096347616" false {"a" "A"}))) ; 111.13
(defn- start-span
"Returns new `io.opentelemetry.api.trace.Span` with random `traceId` and `spanId`."
^Span
[^Tracer tracer ^Context context ^String span-name ^java.time.Instant inst ?parent]
(let [sb (.spanBuilder tracer span-name)]
(when-let [parent ?parent]
(cond
;; Local parent span, etc.
(instance? Span parent) (.setParent sb (.with context ^Span parent))
;; Remote parent context, etc.
(instance? io.opentelemetry.api.trace.SpanContext parent)
(.setParent sb
(.with context
(Span/wrap ^io.opentelemetry.api.trace.SpanContext parent)))
:else
(enc/unexpected-arg! parent
{:context `start-span
:expected
#{io.opentelemetry.api.trace.Span
io.opentelemetry.api.trace.SpanContext}})))
(.setStartTimestamp sb inst)
(.startSpan sb)))
(comment
(let [inst (enc/now-inst)] (enc/qb 1e6 (start-span my-tr (Context/current) "id1" inst nil))) ; 158.09
(start-span my-tr (Context/current) "id1" (enc/now-inst) (start-span my-tr (Context/current) "id2" (enc/now-inst) nil))
(start-span my-tr (Context/current) "id1" (enc/now-inst)
(remote-span-context "c5b856d919f65e39a202bfb3034d65d8" "1111111111111111" false nil)))
(let [ak-uid (io.opentelemetry.api.common.AttributeKey/stringKey "uid")
ak-ns (io.opentelemetry.api.common.AttributeKey/stringKey "ns")
ak-line (io.opentelemetry.api.common.AttributeKey/longKey "line")]
(defn- span-attrs
"Returns `io.opentelemetry.api.common.Attributes` or nil."
[uid signal]
(if uid
(if-let [ns (get signal :ns)]
(if-let [line (get signal :line)]
(Attributes/of ak-uid (str uid), ak-ns ns, ak-line line)
(Attributes/of ak-uid (str uid), ak-ns ns))
(Attributes/of ak-uid (str uid)))
(if-let [ns (get signal :ns)]
(if-let [line (get signal :line)]
(Attributes/of ak-ns ns, ak-line line)
(Attributes/of ak-ns ns))
nil))))
(comment (enc/qb 1e6 (span-attrs "uid1" {:ns "ns1" :line 495}))) ; 100.91
(def ^:private ^String span-name
(enc/fmemoize
(fn [id]
#_(if id (str id) ":telemere/nil-id")
(if id (enc/as-qname id) "telemere/nil-id"))))
(comment (enc/qb 1e6 (span-name :foo/bar))) ; 46.09
(defn- handle-tracing!
"Experimental! Takes care of relevant signal `Span` management.
Returns nil or `io.opentelemetry.api.trace.Span` for possible use as
`io.opentelemetry.api.logs.LogRecordBuilder` context.
Expect:
- `spans_` - latom: {<uid> <Span_>}
- `end-buffer_` - latom: #{[<uid> <end-inst>]}
- `gc-buffer_` - latom: #{<uid>}"
[tracer context spans_ end-buffer_ gc-buffer_ gc-latch_ signal]
;; Notes:
;; - Spans go to `SpanExporter` after `.end` call, ~random order okay
;; - Span data: t1 of self, and name + id + t0 of #{self parent trace}
;; - No API to directly create spans with needed data, so we ~simulate
;; typical usage
(when-let [^java.util.concurrent.CountDownLatch gc-latch (gc-latch_)]
(try (.await gc-latch) (catch InterruptedException _)))
(enc/when-let
[root (get signal :root) ; Tracing iff root
root-uid (get root :uid)
:let [curr-spans (spans_)]
root-span
(force
(or ; Fetch/ensure Span for root
(get curr-spans root-uid)
(when-let [root-inst (get root :inst)]
(let [root-id (get root :id)]
(spans_ root-uid
(fn [old]
(or old
(delay
;; TODO Support remote-span-context parent and/or span links?
(start-span tracer context (span-name root-id)
root-inst nil)))))))))]
(let [?parent-span ; May be identical to root-span
(when-let [parent (get signal :parent)]
(when-let [parent-uid (get parent :uid)]
(if (= parent-uid root-uid)
root-span
(force
(or ; Fetch/ensure Span for parent
(get curr-spans parent-uid)
(let [{parent-id :id, parent-inst :inst} parent]
(spans_ parent-uid
(fn [old]
(or old
(delay
(start-span tracer context (span-name parent-id)
parent-inst root-span)))))))))))
{this-uid :uid, this-end-inst :end-inst} signal]
(enc/cond
;; No end-inst => no run-form =>
;; add `Event` (rather than child `Span`) to parent
:if-let [this-is-event? (not this-end-inst)]
(when-let [^Span parent-span ?parent-span]
(let [{this-id :id, this-inst :inst} signal]
(if-let [^Attributes attrs (span-attrs this-uid signal)]
(.addEvent parent-span (span-name this-id) attrs ^java.time.Instant this-inst)
(.addEvent parent-span (span-name this-id) ^java.time.Instant this-inst)))
(do parent-span))
:if-let
[^Span this-span
(if (= this-uid root-uid)
root-span
(force
(or ; Fetch/ensure Span for this (child)
(get curr-spans this-uid)
(let [{this-id :id, this-inst :inst} signal]
(spans_ this-uid
(fn [old]
(or old
(delay
(start-span tracer context (span-name this-id)
this-inst (or ?parent-span root-span))))))))))]
(do
(if (utils/error-signal? signal)
(.setStatus this-span io.opentelemetry.api.trace.StatusCode/ERROR)
(.setStatus this-span io.opentelemetry.api.trace.StatusCode/OK))
(when-let [^Attributes attrs (span-attrs this-uid signal)]
(.setAllAttributes this-span attrs))
;; Error stuff
(when-let [error (get signal :error)]
(when (instance? Throwable error)
(if-let [attrs
(when-let [ex-data (ex-data error)]
(when-not (empty? ex-data)
(let [sb (Attributes/builder)]
(enc/run-kv! (fn [k v] (put-attr! sb (attr-name k) v)) ex-data)
(.build sb))))]
(.recordException this-span error attrs)
(.recordException this-span error))))
;; (.end this-span this-end-inst) ; Ready for `SpanExporter`
(end-buffer_ (fn [old] (conj old [this-uid this-end-inst])))
(gc-buffer_ (fn [old] (conj old this-uid)))
this-span)))))
(comment
(do
(require '[taoensso.telemere :as t])
(def spans_ "{<uid> <Span_>}" (enc/latom {}))
(def end-buffer_ "#{[<uid> <end-inst>]}" (enc/latom #{}))
(def gc-buffer_ "#{<uid>}" (enc/latom #{}))
(let [[_ [s1 s2]] (t/with-signals (t/trace! ::id1 (t/trace! ::id2 "form2")))]
(def s1 s1)
(def s2 s2)))
[@gc-buffer_ @end-buffer_ @spans_]
(handle-tracing! my-tr spans_ end-buffer_ gc-buffer_ (enc/latom nil) s1))
;;;; Logging
(defn- level->severity
^Severity [level]
(case level
:trace Severity/TRACE
:debug Severity/DEBUG
:info Severity/INFO
:warn Severity/WARN
:error Severity/ERROR
:fatal Severity/FATAL
:report Severity/INFO4
Severity/UNDEFINED_SEVERITY_NUMBER))
(defn- level->string
^String [level]
(when level
(case level
:trace "TRACE"
:debug "DEBUG"
:info "INFO"
:warn "WARN"
:error "ERROR"
:fatal "FATAL"
:report "INFO4"
(str level))))
(defn- signal->attrs
"Returns `io.opentelemetry.api.common.Attributes` for given signal.
Ref. <https://opentelemetry.io/docs/specs/otel/logs/data-model/>."
^Attributes [signal]
(let [ab (Attributes/builder)]
(put-attr! ab "error" (utils/error-signal? signal)) ; Standard
;; (put-attr! ab "host.name" (utils/hostname)) ; Standard
(when-let [{:keys [name ip]} (get signal :host)]
(put-attr! ab "host.name" name) ; Standard
(put-attr! ab "host.ip" ip))
(when-let [level (get signal :level)]
(put-attr! ab "level" ; Standard
(level->string level)))
(when-let [{:keys [type msg trace data]} (enc/ex-map (get signal :error))]
(put-attr! ab "exception.type" type) ; Standard
(put-attr! ab "exception.message" msg) ; Standard
(when trace
(put-attr! ab "exception.stacktrace" ; Standard
(#'utils/format-clj-stacktrace trace)))
(when data (merge-attrs! ab "exception.data" data)))
(let [{:keys [ns line file, kind id uid]} signal]
(put-attr! ab "ns" ns)
(put-attr! ab "line" line)
(put-attr! ab "file" file)
(put-attr! ab "kind" kind)
(put-attr! ab "id" id)
(put-attr! ab "uid" uid))
(when-let [run-form (get signal :run-form)]
(let [{:keys [run-val run-nsecs]} signal]
(put-attr! ab "run.form" (if (nil? run-form) "nil" (str run-form)))
(put-attr! ab "run.val_type" (if (nil? run-val) "nil" (.getName (class run-val))))
(put-attr! ab "run.val" run-val)
(put-attr! ab "run.nsecs" run-nsecs)))
(put-attr! ab "sample" (get signal :sample-rate))
(when-let [{:keys [id uid]} (get signal :parent)]
(put-attr! ab "parent.id" id)
(put-attr! ab "parent.uid" uid))
(when-let [{:keys [id uid]} (get signal :root)]
(put-attr! ab "root.id" id)
(put-attr! ab "root.uid" uid))
(when-let [ctx (get signal :ctx)] (merge-attrs! ab "ctx" ctx))
(when-let [data (get signal :data)] (merge-attrs! ab "data" data))
(when-let [attrs (get signal :otel/attrs)] ; Undocumented
(cond
(map? attrs) (enc/run-kv! (fn [k v] (put-attr! ab (attr-name k) v)) attrs) ; Unprefixed
(instance? Attributes attrs) (.putAll ab ^Attributes attrs) ; Unprefixed
:else
(enc/unexpected-arg! attrs
{:context `signal->attrs!
:expected #{nil map io.opentelemetry.api.common.Attributes}})))
(.build ab)))
(comment
(enc/qb 1e6 ; 850.93
(signal->attrs
{:level :info :data {:ns/kw1 :v1 :ns/kw2 :v2}
:otel/attrs {:longs [1 1 2 3] :strs ["a" "b" "c"]}})))
(defn handler:open-telemetry-logger
"Highly experimental, possibly buggy, and subject to change!!
Feedback and bug reports very welcome! Please ping me (Peter) at:
<https://www.taoensso.com/telemere> or
<https://www.taoensso.com/telemere/slack>
Needs `opentelemetry-java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>.
Returns a signal handler that:
- Takes a Telemere signal (map).
- Emits signal data to configured `io.opentelemetry.api.logs.Logger`
- Emits tracing data to configured `io.opentelemetry.api.logs.Tracer`
Options:
`:logger-provider` - #{nil :default <io.opentelemetry.api.logs.LoggerProvider>} [1]
`:tracer-provider` - #{nil :default <io.opentelemetry.api.trace.TracerProvider>} [1]
`:max-span-msecs` - (Advanced) Longest tracing span to support in milliseconds
(default 120 mins). If recorded spans exceed this max, emitted
data will be inaccurate. Larger values use more memory.
[1] See `get-default-providers` for more info"
;; Notes:
;; - Multi-threaded handlers may see signals ~out of order
;; - Sampling means that root/parent/child signals may never be handled
;; - `:otel/attrs`, `:otel/context` currently undocumented
([] (handler:open-telemetry-logger nil))
([{:keys [logger-provider tracer-provider max-span-msecs]
:or
{logger-provider :default
tracer-provider :default
max-span-msecs (enc/msecs :mins 120)}}]
(let [min-max-span-msecs (enc/msecs :mins 15)]
(when (< (long max-span-msecs) min-max-span-msecs)
(throw
(ex-info "`max-span-msecs` too small"
{:given max-span-msecs, :min min-max-span-msecs}))))
(let [?logger-provider (if (= logger-provider :default) (:logger-provider (force default-providers_)) logger-provider)
?tracer-provider (if (= tracer-provider :default) (:tracer-provider (force default-providers_)) tracer-provider)
?tracer
(when-let [^io.opentelemetry.api.trace.TracerProvider p ?tracer-provider]
(.get p "Telemere"))
;;; Tracing state
spans_ (when ?tracer (enc/latom {})) ; {<uid> <Span_>}
end-buffer1_ (when ?tracer (enc/latom #{})) ; #{[<uid> <end-inst>]}
sgc-buffer1_ (when ?tracer (enc/latom #{})) ; #{<uid>} ; Slow GC
gc-latch_ (when ?tracer (enc/latom nil)) ; ?CountDownLatch
stop-tracing!
(if-not ?tracer
(fn stop-tracing! []) ; Noop
(let [end-buffer2_ (enc/latom #{})
sgc-buffer2_ (enc/latom #{})
fgc-buffer1_ (enc/latom #{})
fgc-buffer2_ (enc/latom #{})
tmax (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimerMax" (boolean :daemon))
t2m (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimer2m" (boolean :daemon))
t3s (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimer3s" (boolean :daemon))
schedule!
(fn [^java.util.Timer timer ^long interval-msecs f]
(.schedule timer (proxy [java.util.TimerTask] [] (run [] (f)))
interval-msecs interval-msecs))
gc-spans!
(fn [uids-to-gc]
(when-not (empty? uids-to-gc)
(let [uids-to-gc (set/intersection uids-to-gc (set (keys (spans_))))]
(when-not (empty? uids-to-gc)
;; ;; Update in small batches to minimize contention
;; (doseq [batch (partition-all 16 uids-to-gc)]
;; (spans_ (fn [old] (reduce dissoc old batch))))
(let [gc-latch (java.util.concurrent.CountDownLatch. 1)]
(when (compare-and-set! gc-latch_ nil gc-latch)
(try
(spans_ (fn [old] (reduce dissoc old uids-to-gc)))
(finally
(.countDown gc-latch)
(reset! gc-latch_ nil)))))))))
move-uids!
(fn [src_ dst_]
(let [drained (enc/reset-in! src_ #{})]
(when-not (empty? drained)
(dst_ (fn [old] (set/union old drained))))))]
;; Notes:
;; - Maintain local {<uid> <Span_>} state, creating spans as needed
;; - A timer+buffer system is used to delay calling `.end` on
;; spans, allowing parents to linger in case they're handled
;; before children.
;;
;; Internal buffer flow:
;; 1. handler->end1->end2->(end!)->fgc1->fgc2->(gc!) ; Fast GC path (span ended)
;; 2. handler ->sgc1->sgc2->(gc!) ; Slow GC path (span not ended)
;;
;; Properties:
;; - End spans 3-6 secs after trace handler ; Linger for possible out-of-order children
;; - GC spans 2-4 mins after ending ; '', children will noop
;; - GC spans 90-92 mins after span first created
;; Final catch-all for spans that may have been created but
;; never ended (e.g. due to sampling or filtering).
;; => Max span runtime!
(schedule! tmax max-span-msecs ; sgc2->(gc!)
(fn [] (gc-spans! (enc/reset-in! sgc-buffer2_ #{}))))
(schedule! t2m (enc/msecs :mins 2)
(fn []
(gc-spans! (enc/reset-in! fgc-buffer2_ #{})) ; fgc2->(gc!)
(move-uids! fgc-buffer1_ fgc-buffer2_) ; fgc1->fgc2
(move-uids! sgc-buffer1_ sgc-buffer2_) ; sgc1->sgc2
))
(schedule! t3s (enc/msecs :secs 3)
(fn []
(let [drained (enc/reset-in! end-buffer2_ #{})]
(when-not (empty? drained)
;; end2->(end!)
(let [spans (spans_)]
(doseq [[uid end-inst] drained]
(when-let [span_ (get spans uid)]
(.end ^Span (force span_) ^java.time.Instant end-inst))))
;; (end!)->fgc1
(let [uids (into #{} (map (fn [[uid _]] uid)) drained)]
(fgc-buffer1_ (fn [old] (set/union old uids))))))
;; end1->end2
(move-uids! end-buffer1_ end-buffer2_)))
(fn stop-tracing! []
(loop [] (when-not (empty? (end-buffer1_)) (recur))) ; Block to drain `end1`
(loop [] (when-not (empty? (end-buffer2_)) (recur))) ; Block to drain `end2`
(.cancel t3s) (.cancel t2m) (.cancel tmax))))]
(fn a-handler:open-telemetry-logger
([ ] (stop-tracing!))
([signal]
(let [?context
(enc/when-let
[^Tracer tracer ?tracer
^Context context
(enc/get* signal :otel/context ; Undocumented
:_otel-context
#_(io.opentelemetry.context.Context/root)
(io.opentelemetry.context.Context/current))
^Span span
(handle-tracing! tracer context
spans_ end-buffer1_ sgc-buffer1_ gc-latch_ signal)]
(.storeInContext span context))]
(when-let [^io.opentelemetry.api.logs.LoggerProvider logger-provider ?logger-provider]
(let [{:keys [ns inst level msg_]} signal
logger (.get logger-provider (or ns "default"))
lrb (.logRecordBuilder logger)]
(.setTimestamp lrb inst)
(.setSeverity lrb (level->severity level))
(.setAllAttributes lrb (signal->attrs signal))
(when-let [^Context context ?context] ; Incl. traceId, SpanId, etc.
(.setContext lrb context))
(when-let [body
(or
(force msg_)
(when-let [error (get signal :error)]
(when (instance? Throwable error)
(str (enc/ex-type error) ": " (enc/ex-message error)))))]
(.setBody lrb body))
;; Ready for `LogRecordExporter`
(.emit lrb)))))))))
(comment
(do
(require '[taoensso.telemere :as t])
(def h1 (handler:open-telemetry-logger))
(let [[_ [s1 s2]] (t/with-signals (t/trace! ::id1 (t/trace! ::id2 "form2")))]
(def s1 s1)
(def s2 s2)))
(h1 s1))

View file

@ -1,163 +0,0 @@
(ns taoensso.telemere.timbre
"Main Timbre macros, reimplemented on top of Telemere.
Intended to help ease migration from Timbre to Telemere."
(:require
[clojure.string :as str]
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.impl :as impl]
[taoensso.telemere :as tel]))
(comment
(remove-ns 'taoensso.telemere.timbre)
(:api (enc/interns-overview)))
(let [arg-str
(fn [x]
(enc/cond
(nil? x) "nil"
(record? x) (pr-str x)
:else x))]
(defn ^:no-doc parse-vargs
"Private, don't use. Adapted from Timbre."
[format-msg? vargs]
(let [[v0] vargs]
(if (enc/error? v0)
(let [error v0
vargs (enc/vrest vargs)
pattern (if format-msg? (let [[v0] vargs] v0) nil)
vargs (if format-msg? (enc/vrest vargs) vargs)
msg
(delay
(if format-msg?
(enc/format* pattern vargs)
(enc/str-join " " (map arg-str) vargs)))]
[error msg {:vargs vargs}])
(let [md (if (and (map? v0) (get (meta v0) :meta)) v0 nil)
error (get md :err)
md (dissoc md :err)
vargs (if md (enc/vrest vargs) vargs)
pattern (if format-msg? (let [[v0] vargs] v0) nil)
vargs (if format-msg? (enc/vrest vargs) vargs)
msg
(delay
(if format-msg?
(enc/format* pattern vargs)
(enc/str-join " " (map arg-str) vargs)))]
[error msg {:vargs vargs}])))))
(comment
(parse-vargs true [ "hello %s" "stu"])
(parse-vargs true [(Exception. "Ex1") "hello %s" "stu"]))
(def ^:no-doc ^:const shim-id :taoensso.telemere/timbre)
#?(:clj
(defmacro ^:no-doc log!
"Private, don't use."
[level format-msg? vargs]
(enc/keep-callsite
`(when (impl/signal-allowed? {:kind :log, :level ~level, :id shim-id})
(let [[error# msg# data#] (parse-vargs ~format-msg? ~vargs)]
(tel/log!
{:allow? true
:level ~level
:id shim-id
:error error#
:data data#}
msg#)
nil)))))
(comment
(macroexpand '(trace "foo"))
(tel/with-signal :force-msg (trace "foo"))
(tel/with-signal :force-msg (infof "Hello %s" "world")))
#?(:clj
(do
(defmacro log "Prefer `telemere/log!`, etc." [level & args] (enc/keep-callsite `(log! ~level false [~@args])))
(defmacro trace "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :trace false [~@args])))
(defmacro debug "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :debug false [~@args])))
(defmacro info "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :info false [~@args])))
(defmacro warn "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :warn false [~@args])))
(defmacro error "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :error false [~@args])))
(defmacro fatal "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :fatal false [~@args])))
(defmacro report "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :report false [~@args])))
(defmacro logf "Prefer `telemere/log!`, etc." [level & args] (enc/keep-callsite `(log! ~level true [~@args])))
(defmacro tracef "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :trace true [~@args])))
(defmacro debugf "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :debug true [~@args])))
(defmacro infof "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :info true [~@args])))
(defmacro warnf "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :warn true [~@args])))
(defmacro errorf "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :error true [~@args])))
(defmacro fatalf "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :fatal true [~@args])))
(defmacro reportf "Prefer `telemere/log!`, etc." [& args] (enc/keep-callsite `(log! :report true [~@args])))))
#?(:clj
(defmacro spy!
"Prefer `telemere/spy!`."
([ form] (enc/keep-callsite `(spy! :debug nil ~form)))
([level form] (enc/keep-callsite `(spy! ~level nil ~form)))
([level form-name form]
(let [msg
(if-not form-name
`impl/default-trace-msg
`(fn [_form# value# error# nsecs#]
(impl/default-trace-msg ~form-name value# error# nsecs#)))]
(enc/keep-callsite
`(tel/spy!
{:kind :spy
:level ~level
:id shim-id
:msg ~msg
:catch->error true}
~form))))))
(comment
(select-keys (tel/with-signal :force-msg (spy! :info "my-form-name" (+ 1 2))) [:level :msg_])
(select-keys (tel/with-signal :force-msg (spy! :info "my-form-name" (throw (Exception. "Ex")))) [:level #_:msg_]))
#?(:clj (defmacro log-errors "Prefer `telemere/catch->error!`." [& body] (enc/keep-callsite `(tel/catch->error! {:id shim-id, :catch-val nil} (do ~@body)))))
#?(:clj (defmacro log-and-rethrow-errors "Prefer `telemere/catch->error!`." [& body] (enc/keep-callsite `(tel/catch->error! {:id shim-id} (do ~@body)))))
#?(:clj (defmacro logged-future "Prefer `telemere/catch->error!`." [& body] (enc/keep-callsite `(future (tel/catch->error! {:id shim-id} (do ~@body))))))
#?(:clj
(defmacro refer-timbre
"(require
'[taoensso.telemere.timbre :as timbre :refer
[log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy]])"
[]
`(require
'~'[taoensso.telemere.timbre :as timbre :refer
[log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy]])))
;;;;
(defn set-min-level! "Prefer `telemere/set-min-level!`." [min-level] (tel/set-min-level! min-level))
#?(:clj
(defmacro with-min-level
"Prefer `telemere/with-min-level`."
[min-level & body]
`(tel/with-min-level ~min-level (do ~@body))))
#?(:clj
(defmacro set-ns-min-level!
"Prefer `telemere/set-min-level!`."
([ ?min-level] `(set-ns-min-level! ~(str *ns*) ~?min-level))
([ns ?min-level] `(tel/set-min-level! nil ~(str ns) ~?min-level))))
#?(:clj (defmacro with-context "Prefer `telemere/with-ctx`." [context & body] `(tel/with-ctx ~context (do ~@body))))
#?(:clj (defmacro with-context+ "Prefer `telemere/with-ctx+`." [context & body] `(tel/with-ctx+ ~context (do ~@body))))
(defn shutdown-appenders!
"Prefer `telemere/stop-handlers!`."
[] (tel/stop-handlers!))

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,8 @@ Its key function is to help:
1. **Capture data** in your running Clojure/Script programs, and 1. **Capture data** in your running Clojure/Script programs, and
2. **Facilitate processing** of that data into **useful information / insight**. 2. **Facilitate processing** of that data into **useful information / insight**.
> [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.
## Signals ## Signals
The basic unit of data in Telemere is the **signal**. The basic unit of data in Telemere is the **signal**.
@ -19,7 +21,7 @@ And they're represented by plain **Clojure/Script maps** with those attributes (
Fundamentally **all signals**: Fundamentally **all signals**:
- Occur or are observed at a particular **location** in your code (file, namespace, line, column). - Occur or are observed at a particular **location** in your code (namespace, line, column).
- Occur or are observed *within* a particular **program state** / context. - Occur or are observed *within* a particular **program state** / context.
- Convey something of value *about* that **program state** / context. - Convey something of value *about* that **program state** / context.
@ -67,7 +69,7 @@ Its name is a combination of _telemetry_ and _telomere_:
> *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. > *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.
> *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. > *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).
# Setup # Setup
@ -81,7 +83,7 @@ deps.edn: com.taoensso/telemere {:mvn/version "x-y-z"}
And setup your namespace imports: And setup your namespace imports:
```clojure ```clojure
(ns my-app (:require [taoensso.telemere :as t])) (ns my-app (:require [taoensso.telemere :as tel]))
``` ```
# Default config # Default config
@ -96,58 +98,63 @@ See section [3-Config](./3-Config) for customization.
> Signal handlers process created signals to *do something with them* (analyse them, write them to console/file/queue/db, etc.) > Signal handlers process created signals to *do something with them* (analyse them, write them to console/file/queue/db, etc.)
| Platform | Condition | Handler | | Platform | Condition | Handler |
| -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Clj | Always | [Console handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) that prints signals to `*out*` or `*err*`. | | Clj | Always | [Console handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) that prints signals to `*out*` or `*err*` |
| Cljs | Always | [Console handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) that prints signals to the **browser console**. | | Cljs | Always | [Console handler](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) that prints signals to the **browser console** |
**Default signal intakes**: **Default interop**:
> Telemere can create signals from relevant **external API calls**, etc. > Telemere can create signals from relevant **external API calls**, etc.
| Platform | Condition | Signals from | | Platform | Condition | Signals from |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| Clj | [SLF4J API](https://mvnrepository.com/artifact/org.slf4j/slf4j-api) and [Telemere SLF4J backend](https://clojars.org/com.taoensso/slf4j-telemere) present | [SLF4J](https://www.slf4j.org/) logging calls. | | 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 |
| 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. | | 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 |
| 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. | | 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 |
Run [`check-intakes`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-intakes) to help verify/debug: 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:
```clojure ```clojure
(check-intakes) ; => (check-interop) ; =>
{:tools-logging {:present? false} {:tools-logging {:present? false}
:slf4j {:sending->telemere? true, :telemere-receiving? true} :slf4j {:present? true, :telemere-receiving? true, ...}
:system/out {:sending->telemere? false, :telemere-receiving? false} :open-telemetry {:present? true, :use-tracer? false, ...}
:system/err {:sending->telemere? false, :telemere-receiving? false}} :system/out {:telemere-receiving? false, ...}
:system/err {:telemere-receiving? false, ...}}
``` ```
# Usage # Usage
## Creating signals ## Creating signals
Use whichever signal creator is most convenient for your needs: 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!`.
| Name | Signal kind | Main arg | Optional arg | Returns | Several different wrapper macros are provided. The only difference between them:
|:-- | :-- | :-- | :-- | :-- |
| [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) | `:log` | `msg` | `opts`/`level` | Signal allowed?
| [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) | `:event` | `id` | `opts`/`level` | Signal allowed?
| [`error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error!) | `:error` | `error` | `opts`/`id` | Given error
| [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!) | `:trace` | `form` | `opts`/`id` | Form result
| [`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) | `:spy` | `form` | `opts`/`level` | Form result
| [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) | `:error` | `form` | `opts`/`id` | Form value or given fallback
| [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) | `<arb>` | `opts` | - | Depends on opts
- See relevant docstrings (links above) for usage info. 1. They create signals with a different `:kind` value (which can be handy for filtering, etc.).
- See [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) for more about signal creators. 2. They have different positional arguments and/or return values optimised for concise calling in different use cases.
- See [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) for options shared by all signal creators.
- See [examples.cljc](https://github.com/taoensso/telemere/blob/master/examples.cljc) for REPL-ready examples. **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!
See the linked docstrings below for more info:
| Name | Args | Returns |
| :---------------------------------------------------------------------------------------------------------- | :------------------------- | :--------------------------- |
| [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) | `[opts]` or `[?level msg]` | nil |
| [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) | `[opts]` or `[id ?level]` | nil |
| [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!) | `[opts]` or `[?id run]` | Form result |
| [`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) | `[opts]` or `[?level run]` | Form result |
| [`error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error!) | `[opts]` or `[?id error]` | Given error |
| [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) | `[opts]` or `[?id error]` | Form value or given fallback |
| [`signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#signal!) | `[opts]` | Depends on opts |
## Checking signals ## Checking signals
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: 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:
```clojure ```clojure
(t/with-signal (tel/with-signal
(t/log! (tel/log!
{:let [x "x"] {:let [x "x"]
:data {:x x}} :data {:x x}}
["My msg:" x])) ["My msg:" x]))
@ -162,31 +169,35 @@ Both have several options, see their docstrings (links above) for details.
## Filtering ## Filtering
A signal will be provided to a handler iff ALL of the following are true: A signal will be provided to a handler iff **ALL** of the following are true:
- 1. Signal **creation** is allowed by **signal filters**: - 1. Signal **call filters** pass:
- a. Compile time: sample rate, kind, ns, id, level, when form, rate limit - a. Compile time: sample rate, kind, ns, id, level, when form, rate limit
- b. Runtime: sample rate, kind, ns, id, level, when form, rate limit - b. Runtime: sample rate, kind, ns, id, level, when form, rate limit
- 2. Signal **handling** is allowed by **handler filters**: - 2. Signal **handler filters** pass:
- a. Compile time: not applicable - a. Compile time: not applicable
- b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit - b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit
- 3. **Signal middleware** `(fn [signal]) => ?modified-signal` does not return nil - 3. **Call transform** `(fn [signal]) => ?modified-signal` returns non-nil
- 4. **Handler middleware** `(fn [signal]) => ?modified-signal` does not return nil - 4. **Handler transform** `(fn [signal]) => ?modified-signal` returns non-nil
> 👉 Transform fns provides a flexible way to modify and/or filter signals by arbitrary signal data/content conditions (return nil to skip handling).
> 👉 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.
Quick examples of some basic filtering: Quick examples of some basic filtering:
```clojure ```clojure
(t/set-min-level! :info) ; Set global minimum level (tel/set-min-level! :info) ; Set global minimum level
(t/with-signal (t/event! ::my-id1 :info)) ; => {:keys [inst id ...]} (tel/with-signal (tel/log! {:level :info ...})) ; => {:keys [inst id ...]}
(t/with-signal (t/event! ::my-id1 :debug)) ; => nil (signal not allowed) (tel/with-signal (tel/log! {:level :debug ...})) ; => nil (signal not allowed)
(t/with-min-level :trace ; Override global minimum level (tel/with-min-level :trace ; Override global minimum level
(t/with-signal (t/event! ::my-id1 :debug))) ; => {:keys [inst id ...]} (tel/with-signal (tel/log! {:level :debug ...})) ; => {:keys [inst id ...]}
;; Disallow all signals in matching namespaces ;; Disallow all signals in matching namespaces
(t/set-ns-filter! {:disallow "some.nosy.namespace.*"}) (tel/set-ns-filter! {:disallow "some.nosy.namespace.*"})
``` ```
- Filtering is always O(1), except for rate limits which are O(n_windows). - Filtering is always O(1), except for rate limits which are O(n_windows).
@ -198,11 +209,11 @@ Quick examples of some basic filtering:
Telemere includes extensive internal help docstrings: Telemere includes extensive internal help docstrings:
| Var | Help with | | Var | Help with |
| :---------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | Creating signals | | [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | Creating signals |
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options when creating signals | | [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options when creating signals |
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal content (map given to middleware/handlers) | | [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal content (map given to transforms/handlers) |
| [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation | | [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation |
| [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management | | [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management |
| [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options | | [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options |
| [`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. | | [`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 |

View file

@ -16,4 +16,4 @@ This flow is visualized below:
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/signal-flow.svg" alt="Telemere signal flowchart" width="640"/> <img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/signal-flow.svg" alt="Telemere signal flowchart" width="640"/>
- `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). - `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).
- The shared **signal middleware 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.). - 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.).

View file

@ -2,21 +2,37 @@ See below for config by topic-
# Filtering # Filtering
A signal will be provided to a handler iff ALL of the following are true: A signal will be provided to a handler iff **ALL** of the following are true:
- 1. Signal **creation** is allowed by **signal filters**: - 1. Signal **call filters** pass:
- a. Compile time: sample rate, kind, ns, id, level, when form, rate limit - a. Compile time: sample rate, kind, ns, id, level, when form, rate limit
- b. Runtime: sample rate, kind, ns, id, level, when form, rate limit - b. Runtime: sample rate, kind, ns, id, level, when form, rate limit
- 2. Signal **handling** is allowed by **handler filters**: - 2. Signal **handler filters** pass:
- a. Compile time: not applicable - a. Compile time: not applicable
- b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit - b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit
- 3. **Signal middleware** `(fn [signal]) => ?modified-signal` does not return nil - 3. **Call transform** `(fn [signal]) => ?modified-signal` returns non-nil
- 4. **Handler middleware** `(fn [signal]) => ?modified-signal` does not return nil - 4. **Handler transform** `(fn [signal]) => ?modified-signal` returns non-nil
> 👉 Transform fns provides a flexible way to modify and/or filter signals by arbitrary signal data/content conditions (return nil to skip handling).
> 👉 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.
See [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) for more about filtering. See [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) for more about filtering.
## Debugging filters
Telemere offers a *lot* of filtering control, so real systems can get quite complex. There's a lot of tools to help debug, including:
| Util | |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| [`with-signal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signal) | To see *last* signal created in body |
| [`with-signals`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-signals) | To see *all* signals created in body |
| [`get-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters) | To see all call filters in current context |
| [`without-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#without-filters) | To disable filters in body |
| [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) | To see handler call stats |
# Signal handlers # Signal handlers
See section [4-Handlers](./4-Handlers). See section [4-Handlers](./4-Handlers).
@ -25,43 +41,41 @@ See section [4-Handlers](./4-Handlers).
## tools.logging ## tools.logging
[`tools.logging`](https://github.com/clojure/tools.logging) can use Telemere as its logging implementation (backend). [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.
To do this: To do this:
1. Ensure that you have the `tools.logging` dependency, and 1. Ensure that you have the tools.logging [dependency](https://mvnrepository.com/artifact/org.clojure/tools.logging), and
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. 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.
Verify successful intake with [`check-intakes`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-intakes): Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
```clojure ```clojure
(check-intakes) ; => (check-interop) ; =>
{:tools-logging {:sending->telemere? true, :telemere-receiving? true}} {:tools-logging {:sending->telemere? true, :telemere-receiving? true}}
``` ```
## Java logging ## Java logging
[`SLF4J`](https://www.slf4j.org/) can use Telemere as its logging backend. [SLF4Jv2](https://www.slf4j.org/) can use Telemere as its logging backend. This'll let SLF4J logging calls create Telemere signals.
To do this, ensure that you have the following dependencies: To do this:
1. Ensure that you have the SLF4J [dependency](https://mvnrepository.com/artifact/org.slf4j/slf4j-api) (v2+ **only**), and
2. Ensure that you have the Telemere SLF4J backend [dependency](https://clojars.org/com.taoensso/telemere-slf4j)
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.
Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
```clojure ```clojure
[org.slf4j/slf4j-api "x.y.z"] ; >= 2.0.0 only! (check-interop) ; =>
[com.taoensso/slf4j-telemere "x.y.z"] {:slf4j {:sending->telemere? true, :telemere-receiving? true}}
``` ```
> 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. > 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.
When `com.taoensso/slf4j-telemere` is on your classpath AND no other SLF4J backends are, SLF4J will direct all its logging calls to Telemere. 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.
Verify successful intake with [`check-intakes`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-intakes):
```clojure
(check-intakes) ; =>
{:slf4j {:sending->telemere? true, :telemere-receiving? true}}
```
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.
In this case logging will be forwarded: In this case logging will be forwarded:
@ -70,60 +84,72 @@ In this case logging will be forwarded:
## System streams ## System streams
The JVM's `System/out` and/or `System/err` streams can be set to flush to Telemere signals. The JVM's `System/out` and/or `System/err` streams can be set so that they'll create Telemere signals when flushed.
To do this, call [`streams->telemere!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#streams-%3Etelemere!). To do this, call [`streams->telemere!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#streams-%3Etelemere!).
Note that Clojure's `*out*`, `*err*` are **not** necessarily automatically affected. Note that Clojure's `*out*`, `*err*` are **not** necessarily automatically affected.
Verify successful intake with [`check-intakes`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-intakes): Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
```clojure ```clojure
(check-intakes) ; => (check-interop) ; =>
{:system/out {:sending->telemere? true, :telemere-receiving? true} {:system/out {:sending->telemere? true, :telemere-receiving? true}
:system/err {:sending->telemere? true, :telemere-receiving? true}} :system/err {:sending->telemere? true, :telemere-receiving? true}}
``` ```
## OpenTelemetry ## OpenTelemetry
> **OpenTelemetry interop is experimental** - feedback very welcome! > [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.
>
> Telemere's OpenTelemetry interop is **experimental** - I'm looking for [feedback](https://www.taoensso.com/telemere/slack) on this feature please! 🙏
Telemere can send signals as correlated [`LogRecords`](https://opentelemetry.io/docs/specs/otel/logs/data-model/) and tracing data to configured JVM [OpenTelemetry](https://opentelemetry.io/) exporters. 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/).
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.
To do this: To do this:
1. Ensure that you have the [OpenTelemetry Java](https://github.com/open-telemetry/opentelemetry-java) dependency. 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).
2. Ensure that OpenTelemetry Java and relevant exporters are [appropriately configured](https://opentelemetry.io/docs/languages/java/configuration/). 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).
3. Use [`handler:open-telemetry-logger`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry-logger) to create an appropriately configured handler, and register it with [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!). 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!).
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.
Once registered, this handler will automatically process relevant Telemere signals to emit detailed log and trace data to your OpenTelemetry exporters. 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).
Note that aside from configuring the exporters, 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. Verify successful interop with [`check-interop`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#check-interop):
```clojure
(check-interop) ; =>
{:open-telemetry {:present? true, :use-tracer? true, :viable-tracer? true}}
```
## Tufte ## Tufte
> [Tufte](https:/www.taoensso.com/tufte) is a simple performance monitoring library for Clojure/Script by the author of Telemere. > [Tufte](https://www.taoensso.com/tufte) is a simple performance monitoring library for Clojure/Script by the author of Telemere.
Telemere can easily incorporate Tufte performance data in its signals, just like any other data: Telemere can easily incorporate Tufte performance data in its signals, just like any other data:
```clojure ```clojure
(let [[_ perf-data] (tufte/profiled <opts> <form>)] (let [[_ perf-data] (tufte/profiled <opts> <form>)]
(t/log! "Performance data" {:perf-data perf-data})) (tel/log! {:perf-data perf-data} "Performance data"))
``` ```
Telemere and Tufte work great together: Telemere and Tufte work great together:
- Their functionality is complementary. - Their functionality is complementary.
- The [upcoming](https:/www.taoensso.com/roadmap) Tufte v4 will share the same core as Telemere and offer an **identical API** for managing filters and handlers. - 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.
## Truss ## Truss
> [Truss](https://www.taoensso.com/truss) is an assertions micro-library for Clojure/Script by the author of Telemere. > [Truss](https://www.taoensso.com/truss) is a micro toolkit for Clojure/Script errors by the author of Telemere.
Telemere can easily incorporate Truss assertion failure information in its signals, just like any other (error) data. Telemere can easily incorporate Truss assertion failure information in its signals, just like any other (error) data.
The [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) signal creator can be particularly convenient for this: The [`catch->error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#catch-%3Eerror!) signal creator can be particularly convenient for this:
```clojure ```clojure
(t/catch->error! <form-with-truss-assertion/s>) (tel/catch->error! <form-with-truss-assertion/s>)
``` ```
Telemere also uses [Truss contextual exceptions](https://cljdoc.org/d/com.taoensso/truss/CURRENT/api/taoensso.truss#ex-info) when relevant.

View file

@ -1,33 +1,43 @@
Signal handlers process created signals to **do something with them** (analyse them, write them to console/file/queue/db, etc.). 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.).
Telemere includes a number of signal handlers out-the-box, and more may be available via the [community](./8-Community#handlers). Here's a minimal handler: `(fn [signal] (println signal))`.
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.:
```
(fn my-handler
([signal] (println signal)
([] (my-stop-code)))
```
Telemere includes a number of signal handlers out-the-box, and more may be available via the [community](./8-Community#handlers-and-tools).
You can also easily [write your own handlers](#writing-handlers) for any output or integration you need. You can also easily [write your own handlers](#writing-handlers) for any output or integration you need.
# Included handlers # Included handlers
Alphabetically (see linked docstrings below for features and usage): See ✅ links below for **features and usage**,
See ❤️ links below to **vote on future handlers**:
| Name | Platform | Output target | Output format | | Target (↓) | Clj | Cljs |
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | :--------------------------------------------- | :-----------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------: |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | [Apache Kafka](https://kafka.apache.org/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | [AWS Kinesis](https://aws.amazon.com/kinesis/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals for [cljs-devtools](https://github.com/binaryage/cljs-devtools), etc. | | 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) |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | Console (raw) | - | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) |
| [`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](https://github.com/open-telemetry/opentelemetry-java) exporters | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) and tracing data | | [Datadog](https://www.datadoghq.com/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | [❤️](https://github.com/taoensso/roadmap/issues/12) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | Email | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | - |
| [`handler:slack`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) | Clj | [Slack](https://slack.com/) (via [clj-slack](https://github.com/julienXX/clj-slack)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | File/s | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | - |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | [Graylog](https://graylog.org/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) | | [Jaeger](https://www.jaegertracing.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| [Logstash](https://www.elastic.co/logstash) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
Planned (upcoming) handlers: | [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) |
| [Redis](https://redis.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| Name | Platform | Output target | Output format | | SQL | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
| :----------------------------------------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------- | :----------------------------------------------- | | [Slack](https://slack.com/) | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) | - |
| [`handler:carmine`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.carmine#handler:carmine) | Clj | [Redis](https://redis.io/) (via [Carmine](https://www.taoensso.com/carmine)) | [Serialized](https://taoensso.com/nippy) signals | | TCP socket | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | - |
| [`handler:logstash`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.logstash#handler:logstash) | Clj | [Logstash](https://www.elastic.co/logstash) | TODO | | UDP socket | [](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | - |
| [Zipkin](https://zipkin.io/) | [❤️](https://github.com/taoensso/roadmap/issues/12) | - |
It helps to know what people need! You can [vote on](https://www.taoensso.com/roadmap/vote) additional handlers to add, [ping me](https://github.com/taoensso/telemere/issues), or ask on the [`#telemere` Slack channel](https://www.taoensso.com/telemere/slack).
# Configuring handlers # Configuring handlers
@ -38,21 +48,21 @@ There's two kinds of config relevant to all signal handlers:
## Dispatch opts ## Dispatch opts
Handler dispatch opts includes dispatch priority (determines order in which handlers are called), handler filtering, handler middleware, a/sync queue semantics, back-pressure opts, etc. 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.
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. 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.
Note that handler middleware in particular is an often overlooked but powerful feature, allowing you to arbitrarily transform 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. 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.
## Handler-specific opts ## Handler-specific opts
Handler-specific opts are specified when calling a particular **handler constructor** (like [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CONSOLE/api/taoensso.telemere#handler:console)) - and documented by the constructor. 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.
Note that it's common for Telemere handlers to be customized by providing *Clojure/Script functions* to the relevant handler constructor call. Note that it's common for Telemere handlers to be customized by providing *Clojure/Script functions* to the relevant handler constructor call.
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. 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.
### Example ### Console handler
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. 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.
@ -61,42 +71,46 @@ By default it writes formatted strings intended for human consumption:
```clojure ```clojure
;; Create a test signal ;; Create a test signal
(def my-signal (def my-signal
(t/with-signal (tel/with-signal
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message"))) (tel/log! {:id ::my-id, :data {:x1 :x2}} "My message")))
;; Create console handler with default opts (writes formatted string) ;; Create console handler with default opts (writes formatted string)
(def my-handler (t/handler:console)) (def my-handler (tel/handler:console {}))
;; Test handler, remember it's just a (fn [signal]) ;; Test handler, remember it's just a (fn [signal])
(my-handler my-signal) ; %> (my-handler my-signal) ; %>
;; 2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message ;; 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My message
;; data: {:x1 :x2} ;; data: {:x1 :x2}
``` ```
To instead writes signals as edn: #### edn output
To instead writes signals as [edn](https://github.com/edn-format/edn):
```clojure ```clojure
;; Create console which writes edn ;; Create console handler which writes signals as edn
(def my-handler (def my-handler
(t/handler:console (tel/handler:console
{:output-fn (t/pr-signal-fn {:pr-fn :edn})})) {:output-fn (tel/pr-signal-fn {:pr-fn :edn})}))
(my-handler my-signal) ; %> (my-handler my-signal) ; %>
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...} ;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}
``` ```
#### JSON output
To instead writes signals as JSON: To instead writes signals as JSON:
```clojure ```clojure
;; Create console which writes signals as JSON ;; Ref. <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista])) #?(:clj (require '[jsonista.core :as jsonista]))
(def my-handler (def my-handler
(t/handler:console (tel/handler:console
{:output-fn {:output-fn
(t/pr-signal-fn (tel/pr-signal-fn
{:pr-fn {:pr-fn
#?(:cljs :json #?(:cljs :json ; Use js/JSON.stringify
:clj jsonista.core/write-value-as-string)})})) :clj jsonista/write-value-as-string)})}))
(my-handler my-signal) ; %> (my-handler my-signal) ; %>
;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...} ;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...}
@ -106,27 +120,28 @@ Note that when writing JSON with Clojure, you *must* provide an appropriate `pr-
### Handler-specific per-signal kvs ### Handler-specific per-signal kvs
Telemere includes a handy mechanism for including arbitrary app-level data/opts in individual signals for use by custom middleware and/or handlers. Telemere includes a handy mechanism for including arbitrary app-level data/opts in individual signals for use by custom transforms and/or handlers.
Any *non-standard* (app-level) keys you include in your signal constructor opts will automatically be included in created signals, e.g.: Any *non-standard* (app-level) keys you include in your signal constructor opts will automatically be included in created signals, e.g.:
```clojure ```clojure
(t/with-signal (tel/with-signal
(t/event! ::my-id (tel/log!
{:my-middleware-data "foo" {...
:my-handler-data "bar"})) :my-data-for-xfn "foo"
:my-data-for-handler "bar"}))
;; %> ;; %>
;; {;; App-level kvs included inline (assoc'd to signal root) ;; {;; App-level kvs included inline (assoc'd to signal root)
;; :my-middleware-data "foo" ;; :my-data-for-xfn "foo"
;; :my-handler-data "bar" ;; :my-data-for-handler "bar"
;; :kvs ; And also collected together under ":kvs" key ;; :kvs ; And also collected together under ":kvs" key
;; {:my-middleware-data "foo" ;; {:my-data-for-xfn "foo"
;; :my-handler-data "bar"} ;; :my-data-for-handler "bar"}
;; ... } ;; ... }
``` ```
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 middleware/handlers. 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.
# Managing handlers # Managing handlers
@ -172,9 +187,9 @@ Writing your own signal handlers for Telemere is straightforward, and a reasonab
- Handlers just plain Clojure/Script fns of 2 arities: - Handlers just plain Clojure/Script fns of 2 arities:
```clojure ```clojure
(defn my-basic-handler (defn my-handler
([signal] (println signal)) ; Arity-1 called when handling a signal ([signal] (println signal)) ; Arity-1 called when handling a signal
([]) ; Arity-0 called when stopping the handler ([] (my-stop-code)) ; Arity-0 called when stopping the handler
) )
``` ```
@ -202,7 +217,7 @@ If you're making a customizable handler for use by others, it's often handy to d
;; Do option validation and other prep here, i.e. try to keep ;; Do option validation and other prep here, i.e. try to keep
;; expensive work outside handler function when possible! ;; expensive work outside handler function when possible!
(let [handler-fn ; Fn of exactly 2 arities (let [handler-fn ; Fn of exactly 2 arities (1 and 0)
(fn a-handler:my-fancy-handler ; Note fn naming convention (fn a-handler:my-fancy-handler ; Note fn naming convention
([signal] ; Arity-1 called when handling a signal ([signal] ; Arity-1 called when handling a signal
@ -221,7 +236,7 @@ If you're making a customizable handler for use by others, it's often handy to d
(with-meta handler-fn (with-meta handler-fn
{:dispatch-opts {:dispatch-opts
{:min-level :info {:min-level :info
:rate-limit :limit
[[1 1000] ; Max 1 signal per second [[1 1000] ; Max 1 signal per second
[10 60000] ; Max 10 signals per minute [10 60000] ; Max 10 signals per minute
]}})))) ]}}))))
@ -236,7 +251,7 @@ If you're making a customizable handler for use by others, it's often handy to d
# Example output # Example output
```clojure ```clojure
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message") => (tel/log! {:id ::my-id, :data {:x1 :x2}} "My message") =>
``` ```
## Clj console handler ## Clj console handler
@ -244,7 +259,7 @@ If you're making a customizable handler for use by others, it's often handy to d
[API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | string output: [API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | string output:
``` ```
2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message 2024-04-11T10:54:57.202869Z INFO LOG MyHost examples(56,1) ::my-id - My message
data: {:x1 :x2} data: {:x1 :x2}
``` ```

View file

@ -5,11 +5,11 @@ While [Timbre](https://taoensso.com/timbre) will **continue to be maintained and
Telemere's functionality is a **superset of Timbre**, and offers *many* improvements including: Telemere's functionality is a **superset of Timbre**, and offers *many* improvements including:
- Better support for [structured logging](./1-Getting-started#data-types-and-structures) - Better support for [structured logging](./1-Getting-started#data-types-and-structures)
- Better [performance](https://github.com/taoensso/telemere#benchmarks) - Better [performance](https://github.com/taoensso/telemere#performance)
- Better [documentation](https://github.com/taoensso/telemere#documentation) - Better [documentation](https://github.com/taoensso/telemere#documentation)
- Better [included handlers](./4-Handlers##included-handlers)
- A more flexible [API](./1-Getting-started#usage) that unifies all telemetry and logging needs - A more flexible [API](./1-Getting-started#usage) that unifies all telemetry and logging needs
- A more robust [architecture](./2-Architecture), free from all historical constraints - A more robust [architecture](./2-Architecture), free from all historical constraints
- Better [included handlers](./4-Handlers##included-handlers)
- Easier [configuration](./3-Config) - Easier [configuration](./3-Config)
Migrating from Timbre to Telemere should be straightforward **unless you depend on specific/custom appenders** that might not be available for Telemere (yet). Migrating from Timbre to Telemere should be straightforward **unless you depend on specific/custom appenders** that might not be available for Telemere (yet).
@ -20,7 +20,7 @@ Migrating from Timbre to Telemere should be straightforward **unless you depend
Where Timbre uses the term "appender", Telemere uses the more general "handler". Functionally they're the same thing. Where Timbre uses the term "appender", Telemere uses the more general "handler". Functionally they're the same thing.
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). 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).
If not, you may need to [write something yourself](./4-Handlers#writing-handlers). If not, you may need to [write something yourself](./4-Handlers#writing-handlers).
@ -28,9 +28,21 @@ This may be easier than it sounds. Remember that signals are just plain Clojure/
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). 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).
### 2. Imports ### 2. Logging calls
Switch your Timbre namespace imports: What about all the Timbre logging calls in your code?
You've got two choices-
#### 2a. Redirect Timbre output to Telemere
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.
In this case you may want to disable all your other Timbre appenders, and all your Timbre filtering.
#### 2b. Change your ns imports
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:
```clojure ```clojure
(ns my-ns (ns my-ns
@ -39,9 +51,9 @@ Switch your Timbre namespace imports:
) )
``` ```
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. In this case your Timbre appenders and filtering will be ignored.
Feel free to keep using this 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. 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.
### 3. Config ### 3. Config
@ -63,7 +75,7 @@ If for any reason your tests are unsuccessful, please don't feel pressured to mi
# From tools.logging # From tools.logging
This is easy, see [here](./3-Config#clojuretoolslogging). This is easy, see [here](./3-Config#toolslogging).
# From Java logging # From Java logging

View file

@ -1,6 +1,6 @@
# Does Telemere replace Timbre? # Does Telemere replace Timbre?
> [Timbre](https:/www.taoensso.com/timbre) is a pure Clojure/Script logging library, and ancestor of Telemere. > [Timbre](https://www.taoensso.com/timbre) is a pure Clojure/Script logging library, and ancestor of Telemere.
**Yes**, Telemere's functionality is a **superset of Timbre**, and offers *many* improvements over Timbre. **Yes**, Telemere's functionality is a **superset of Timbre**, and offers *many* improvements over Timbre.
@ -12,7 +12,7 @@ See section [5-Migrating](./5-Migrating#from-timbre) for migration info.
# Why not just update Timbre? # Why not just update Timbre?
> [Timbre](https:/www.taoensso.com/timbre) is a pure Clojure/Script logging library, and ancestor of Telemere. > [Timbre](https://www.taoensso.com/timbre) is a pure Clojure/Script logging library, and ancestor of Telemere.
Why release Telemere as a *new library* instead of just updating Timbre? Why release Telemere as a *new library* instead of just updating Timbre?
@ -26,15 +26,15 @@ That eventually grew into Telemere. And I'm happy enough with the result that I
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. 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.
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 will eventually be used by Telemere, Timbre, and also [Tufte](https://taoensso.com/tufte). 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).
This will eventually ease long-term maintenance, increase reliability, and help provide unified capabilities across all 3. This will eventually ease long-term maintenance, increase reliability, and help provide unified capabilities across all 3.
# Does Telemere replace Tufte? # Does Telemere replace Tufte?
> [Tufte](https:/www.taoensso.com/tufte) is a simple performance monitoring library for Clojure/Script by the author of Telemere. > [Tufte](https://www.taoensso.com/tufte) is a simple performance monitoring library for Clojure/Script by the author of Telemere.
**No**, Telemere does **not** replace [Tufte](https:/www.taoensso.com/tufte). They work great together, and the [upcoming](https:/www.taoensso.com/roadmap) Tufte v4 will share the same core as Telemere and offer an **identical API** for managing filters and handlers. **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.
There is **some feature overlap** though since Telemere offers basic performance measurement as part of its tracing features. There is **some feature overlap** though since Telemere offers basic performance measurement as part of its tracing features.
@ -54,13 +54,13 @@ They're focused on complementary things. When both are in use:
> [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. > [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.
Yes, this shouldn't be a problem. **Yes**, this shouldn't be a problem.
# Does Telemere work with Babashka? # Does Telemere work with Babashka?
> [Babashka](https://github.com/babashka/babashka) is a native Clojure interpreter for scripting with fast startup. > [Babashka](https://github.com/babashka/babashka) is a native Clojure interpreter for scripting with fast startup.
Not currently, though support should be possible with a little work. The current bottleneck is a dependency on [Encore](https://github.com/taoensso/encore), though that could actually be removed (also offering benefits re: library size). **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.
If there's interest in this, please [upvote](https://github.com/taoensso/roadmap/issues/22) on my open source roadmap. If there's interest in this, please [upvote](https://github.com/taoensso/roadmap/issues/22) on my open source roadmap.
@ -76,18 +76,18 @@ Examples:
```clojure ```clojure
;; A fixed message (string arg) ;; A fixed message (string arg)
(t/log! "A fixed message") ; %> {:msg "A fixed message"} (tel/log! "A fixed message") ; %> {:msg "A fixed message"}
;; A joined message (vector arg) ;; A joined message (vector arg)
(let [user-arg "Bob"] (let [user-arg "Bob"]
(t/log! ["User" (str "`" user-arg "`") "just logged in!"])) (tel/log! ["User" (str "`" user-arg "`") "just logged in!"]))
;; %> {:msg_ "User `Bob` just logged in!` ...} ;; %> {:msg_ "User `Bob` just logged in!` ...}
;; With arg prep ;; With arg prep
(let [user-arg "Bob" (let [user-arg "Bob"
usd-balance-str "22.4821"] usd-balance-str "22.4821"]
(t/log! (tel/log!
{:let {:let
[username (clojure.string/upper-case user-arg) [username (clojure.string/upper-case user-arg)
usd-balance (parse-double usd-balance-str)] usd-balance (parse-double usd-balance-str)]
@ -100,10 +100,10 @@ Examples:
;; %> {:msg "User BOB has balance: $22" ...} ;; %> {:msg "User BOB has balance: $22" ...}
(t/log! (str "This message " "was built " "by `str`")) (tel/log! (str "This message " "was built " "by `str`"))
;; %> {:msg "This message was built by `str`"} ;; %> {:msg "This message was built by `str`"}
(t/log! (format "This message was built by `%s`" "format")) (tel/log! (format "This message was built by `%s`" "format"))
;; %> {:msg "This message was built by `format`"} ;; %> {:msg "This message was built by `format`"}
``` ```
@ -113,13 +113,13 @@ See also [`msg-skip`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/tao
# How to use Telemere from a library? # How to use Telemere from a library?
See section [9-Maintainers](./9-Maintainers). See section [9-Authors](./9-Authors.md).
# How does Telemere compare to Mulog? # How does Telemere compare to μ/log?
> [Mulog](https://github.com/BrunoBonacci/mulog) is an excellent "micro-logging library" for Clojure that shares many of the same capabilities and objectives as Telemere. > [μ/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.
Some **similarities** between Telemere and Mulog: Some **similarities** between Telemere and μ/log:
- Both emphasize **structured data** rather than string messages - Both emphasize **structured data** rather than string messages
- Both offer **tracing** to understand (nested) program flow - Both offer **tracing** to understand (nested) program flow
@ -127,7 +127,7 @@ Some **similarities** between Telemere and Mulog:
- Both are **fast** and offer **async handling** - Both are **fast** and offer **async handling**
- Both offer a variety of **handlers** and are designed for ease of use - Both offer a variety of **handlers** and are designed for ease of use
Some particular **strengths of Mulog** that I'm aware of: Some particular **strengths of μ/log** that I'm aware of:
- More **established/mature** - More **established/mature**
- Wider **range of handlers** (incl. Kafka, Kinesis, Prometheus, Zipkin, etc.) - Wider **range of handlers** (incl. Kafka, Kinesis, Prometheus, Zipkin, etc.)
@ -137,7 +137,7 @@ Some particular **strengths of Mulog** that I'm aware of:
Some particular **strengths of Telemere**: Some particular **strengths of Telemere**:
- Both **Clj and Cljs support** (Mulog is Clj only) - Both **Clj and Cljs support** (μ/log is Clj only)
- Rich **filtering capabilities** (see [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters)) incl. compile-time elision - Rich **filtering capabilities** (see [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters)) incl. compile-time elision
- Rich **dispatch control** (see [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options)) - Rich **dispatch control** (see [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options))
- Rich **environmental config** (see [`help:environmental-config`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:environmental-config)) for all platforms - Rich **environmental config** (see [`help:environmental-config`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:environmental-config)) for all platforms
@ -148,7 +148,7 @@ Some particular **strengths of Telemere**:
**My subjective thoughts**: **My subjective thoughts**:
Mulog 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! μ/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!
The two libraries have many shared capabilities and objectives. The two libraries have many shared capabilities and objectives.
@ -159,6 +159,36 @@ Ultimately I wrote Telemere because:
3. I wanted something that integrated particularly well with [Tufte](https://taoensso.com/tufte) and could share an identical API for filtering, handlers, etc. 3. I wanted something that integrated particularly well with [Tufte](https://taoensso.com/tufte) and could share an identical API for filtering, handlers, etc.
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). 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).
# Why the unusual arg order for `event!`?
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.
Why the apparent inconsistency?
It's an intentional trade-off. `event!` is unique in 3x ways:
1. Its primary argument is typically very short (just an id keyword).
2. Its primary argument never depends on `:let` bindings.
3. Its opts typically include long or even multi-lined `:data`.
If `event!` shared the same arg order as other signal creators, the common case would be something like `(event! {:data <multi-line>} ::dangling-id)` which gets unnecessarily awkward and doesnt read well IMO. I want to know what event were talking about, before you tell me about the associated data.
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 […]} <message depending on let bindings>)`. 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.
So basically the choice in trade-off was:
1. Prefer **consistency**, or
2. Prefer **ergonomics** of the common case usage
I went with option 2 for several reasons:
- There _is_ actually consistency, its just not as obvious - the typically-larger argument always goes _last_.
- Most IDEs generally do a good job of reminding about the arg order.
- 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.
- 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).
If theres popular demand, Id also be happy to add something like `ev!` which could choose the alternative trade-off. Though Id recommend folks try `event!` as-is first, since I think the initial aversion/surprise might wear off with use.
# Other questions? # Other questions?
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) 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)

View file

@ -80,9 +80,9 @@ Consider the [differences](https://www.youtube.com/watch?v=oyLBGkS5ICk) between
This way you can see all your ids in one place, and precise info on when ids were added/removed/changed. This way you can see all your ids in one place, and precise info on when ids were added/removed/changed.
- Use **signal middleware** to your advantage. - Use **signal call transforms** to your advantage.
The result of signal middleware is cached and *shared between all handlers* making it an efficient place to transform signals. For this reason - prefer signal middleware to handler middleware when possible/convenient. 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.
- Signal and handler **sampling is multiplicative**. - Signal and handler **sampling is multiplicative**.
@ -90,15 +90,16 @@ Consider the [differences](https://www.youtube.com/watch?v=oyLBGkS5ICk) between
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%). 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%).
This multiplicative rate is helpfully reflected in each signal's final `:sample-rate` value, making it possible to estimate *unsampled* cardinalities in relevant cases. 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.
So for `n` randomly sampled signals matching some criteria, you'd have seen an estimated `Σ(1.0/sample-rate_i)` such signals _without_ sampling, etc. - 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.
- Middleware can return any type, but it's best to return only `nil` or a map. - Transforms can be used to **filter signals** by returning `nil`.
- Middleware can be used to **filter signals** by returning `nil`. - Transforms can be used to **split signals**:
- Middleware can be used to **split signals**.
Your middleware 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. 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.
See also the [`dispatch-signal!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#dispatch-signal!) util.
- Levels can be **arbitrary integers**. - Levels can be **arbitrary integers**.
@ -113,13 +114,13 @@ Consider the [differences](https://www.youtube.com/watch?v=oyLBGkS5ICk) between
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: 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:
```clojure ```clojure
(t/with-signal (t/log! {:my-key "foo"} "My message"))) (tel/with-signal (tel/log! {:my-key "foo"} "My message")))
;; => {:my-key "foo", :kvs {:my-key "foo", ...}, ...} ;; => {:my-key "foo", :kvs {:my-key "foo", ...}, ...}
``` ```
Note that all app-level kvs will *also* be available *together* under the signal's `:kvs` key. Note that all app-level kvs will *also* be available *together* under the signal's `:kvs` key.
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 middleware or handlers. 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.
- Signal `kind` can be useful in advanced cases. - Signal `kind` can be useful in advanced cases.

View file

@ -1,25 +1,27 @@
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, middleware, and utils. 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.
**PRs very welcome** to add links to this page! [PRs](../wiki#contributions-welcome) **very welcome** to add links to this page!
If you spot issues with any linked resources, please **contact the relevant authors** to let them know! Thank you! 🙏 - [Peter](https://www.taoensso.com) If you spot issues with any linked resources, please **contact the relevant authors** to let them know! Thank you! 🙏 - [Peter](https://www.taoensso.com)
# Handlers
Includes libraries or examples for handlers (see [Writing handlers](./4-Handlers#writing-handlers)), middleware, handler utils (e.g. formatters), etc.:
| Date | Link | Description |
| :--- | :--- | :------------------------------------------------------------ |
| - | - | Your link here? [PRs](../wiki#contributions-welcome) welcome! |
# Learning # Learning
Includes videos, tutorials, demo projects, etc.: Includes videos, tutorials, demo projects, etc.
| Date | Link | Description | | Type | Description |
| :--------- | :---------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | | ------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| - | - | Your link here? [PRs](../wiki#contributions-welcome) welcome! | | Support | [Official Slack channel](https://www.taoensso.com/telemere/slack) for questions, help, etc. |
| - | [Official Slack channel](https://www.taoensso.com/telemere/slack) | For questions, support, etc. | | Support | [Official GitHub issues](https://github.com/taoensso/telemere/issues) for questions, help, bug reports, PRs, etc. |
| - | [GitHub issues](https://github.com/taoensso/telemere/issues) | For questions, support, bug reports, PRs, etc. | | Example | [Gist](https://gist.github.com/ptaoussanis/f8a80f85d3e0f89b307a470ce6e044b5) showing use with [Bling](https://github.com/paintparty/bling) (2024-12-23) |
| 2024-06-12 | [YouTube](https://www.youtube.com/watch?v=uyApiNg6h7Y) | [Los Angeles Clojure Users Group](https://www.meetup.com/los-angeles-clojure-users-group/) collaborative learning session (107 mins) | | Example | [Gist](https://gist.github.com/xlfe/e9e2cf23bd1dddcbb2fbd77ce31dcc8b) showing use with **Google Cloud Platform** (GCP) (2024-10-13) |
| 2024-04-18 | [YouTube](https://www.youtube.com/watch?v=-L9irDG8ysM) | Official Telemere announcement demo (24 mins) | | 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) |
| Demo | [Official YouTube demo](https://www.youtube.com/watch?v=-L9irDG8ysM) for Telemere's launch (24 mins) (2024-04-18) |
# Handlers and tools
Includes libraries or examples for handlers (see [Writing handlers](./4-Handlers#writing-handlers)), transforms, handler utils (e.g. formatters), tools for analyzing signals, etc.
| Type | Description |
| ------- | :------------------------------------------------------------ |
| Handler | [Axiom.co](https://github.com/marksto/telemere.axiom) handler |
| - | Your link here? [PRs](../wiki#contributions-welcome) welcome! |

49
wiki/9-Authors.md Normal file
View file

@ -0,0 +1,49 @@
Are you a library author/maintainer that's considering **using Telemere in your library**?
You have **a few options** below-
# Options
## Modern logging facade
[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.
Basically:
1. You include the (very small) Trove dependency with your library
2. Your library logs using the [Trove API](https://github.com/taoensso/trove#to-choose-a-backend)
3. Your users then [choose](https://github.com/taoensso/trove#to-choose-a-backend) their preferred backend (Telemere, etc.)
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.
## Traditional logging facade (basic logging only)
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/).
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)).
## Telemere as a transitive dependency
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.
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.
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:
```clojure
(defn set-min-log-level!
"Sets Telemere's minimum level for <my-lib> namespaces.
This will affect all signals (logs) created by <my-lib>.
Possible minimum levels (from most->least verbose):
#{:trace :debug :info :warn :error :fatal :report}.
The default minimum level is `:warn`."
[min-level]
(tel/set-min-level! nil "my-lib-ns(.*)" min-level)
true)
(defonce ^:private __set-default-log-level (set-min-log-level! :warn))
```
This way your users can easily disable, decrease, or increase signal output from your library without even needing to touch Telemere or to be aware of its existence.

View file

@ -1,78 +0,0 @@
Are you a library maintainer that's considering **using Telemere in your library**?
See below for some considerations and advice-
# Consider a facade
Ask yourself the question: do you specifically *need/want* Telemere?
Many libraries only need very basic logging. In these cases it can be beneficial to do your logging through a facade like [tools.logging](https://github.com/clojure/tools.logging) or [SLF4J](https://www.slf4j.org/).
**Upside**: users can then easily choose and configure their **preferred backend** (including Telemere, which can easily [act as a backend](./3-Config#interop) for both tools.logging and SLF4J).
**Downside**: your logging features will necessarily be limited to the lowest-common-denominator features supported by your chosen facade (tools.logging or SLF4J, etc.). In particular, you'll be giving up support for Telemere's structured logging features and [rich filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters) (including filtering or setting minimum levels by namespaces).
# Consider API stability
Telemere is still currently in **beta** as of May 2024.
While the [signal creator](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) and [signal content](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) APIs should already be mostly stable, I would still **recommend against** using Telemere in any public libraries until after Telemere's **stable v1 release** (current ETA >= [August 2024](https://taoensso.com/roadmap)).
# Using Telemere in your library
If you **do** need/want support for Telemere's structured logging features and/or [rich filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-filters), then you've got a couple options-
## Telemere as a non-optional dependency
This is straight-forward: you include Telemere in your library's dependencies, and you can make use of Telemere's full API from your own library.
Telemere's [default config](./1-Getting-started#default-config) is sensible (with println-like console output), so many of library users won't need to configure or interact with Telemere at all.
The most common thing library users may want to do is **adjust the minimum level** of signals created by your library. And since your users might not be familiar with Telemere, I'd recommend including something like the following in a convenient place like your library's main API namespace:
```clojure
(defn set-min-log-level!
"Sets minimum level of Telemere signals (logs) created by <my-library>.
Possible levels (from most to least verbose):
#{:trace :debug :info :warn :error :fatal :report}.
The default level is `:warn`."
[level]
(tel/set-min-level! nil "my-library.namespace" min-level)
(tel/set-min-level! nil "my-library.namespace.*" min-level)
nil)
(defonce ^:private __set-default-log-level (set-min-log-level! :warn))
```
This way your users can easily disable, decrease, or increase signal output from your library without even needing to touch Telemere or to be aware of its existence.
## Telemere as an optional dependency
I have a solution planned for this that I'm still testing. Will add more info prior to Telemere's [stable v1 release](https://www.taoensso.com/roadmap).
# Migrating from Timbre
Do you have a library that currently uses [Timbre](https://www.taoensso.com/timbre), and you're considering a change to Telemere?
## With a facade
First, I'd encourage you to [consider a facade](#consider-a-fascade).
Unless you specifically want features *only* available to Telemere/Timbre, using a facade would give your users the option to choose **whichever backend they prefer**.
Since both Telemere and Timbre support [tools.logging](https://github.com/clojure/tools.logging) and [SLF4J](https://www.slf4j.org/), either one of those would be a reasonable choice.
(Though note that Timbre's current SLF4J support is a little fragile. Timbre v7 [will introduce](https://github.com/taoensso/roadmap/issues/11) improved native SLF4J support, so you may want to wait for that if you are considering SLF4J).
Example migration steps:
1. Migrate your library's Timbre logging calls to equivalent [`tools.logging`](https://github.com/clojure/tools.logging) calls.
2. Remove your library's Timbre dependency.
3. Advise your users about the dropped dependency, and tell them that they'll now need to opt-in to [use Telemere](https://github.com/taoensso/telemere/wiki/3-Config#toolslogging), [use Timbre](https://taoensso.github.io/timbre/taoensso.timbre.tools.logging.html#var-use-timbre), or use some other logging backend for tools.logging that they prefer.
## Without a facade
In this case you'll need to decide if you want to use Telemere as an [optional](#telemere-as-an-optional-dependency) or [non-optional](#telemere-as-a-non-optional-dependency) dependency.
Will add more info prior to Telemere's [stable v1 release](https://www.taoensso.com/roadmap).