mirror of
https://github.com/taoensso/telemere.git
synced 2026-02-06 20:23:13 +00:00
Compare commits
285 commits
v1.0.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
798e0fddc4 | ||
|
|
6030c472ae | ||
|
|
1d6bdaf7e5 | ||
|
|
6cc54527f2 | ||
|
|
56e35f3f58 | ||
|
|
a6fc4adf6a | ||
|
|
6155713fde | ||
|
|
a883df3c41 | ||
|
|
e6ce33dd4e | ||
|
|
cc680b06f5 | ||
|
|
47af80319d | ||
|
|
b56e1c4529 | ||
|
|
8a3ae14f45 | ||
|
|
917b1b408e | ||
|
|
125e006753 | ||
|
|
f7006f31fe | ||
|
|
c6a71652d7 | ||
|
|
4cc4f45e7c | ||
|
|
ff9e3f4007 | ||
|
|
eb28d365a8 | ||
|
|
d9ad1ba379 | ||
|
|
b2a8b66cc0 | ||
|
|
1bcd46adf3 | ||
|
|
f6ec872f7c | ||
|
|
b7b3a25a82 | ||
|
|
070fe88abb | ||
|
|
dcfeba5b91 | ||
|
|
9d655bb9ce | ||
|
|
75a90c6b6d | ||
|
|
269c58d8fe | ||
|
|
7603ae2fcf | ||
|
|
6fb18bd3b9 | ||
|
|
d6264afe7c | ||
|
|
f08b60bce4 | ||
|
|
3746de8039 | ||
|
|
1bdb667b6c | ||
|
|
2e0a2938b7 | ||
|
|
9d040d70cd | ||
|
|
475e5ba6c2 | ||
|
|
51e8a1062f | ||
|
|
31a4fc26d2 | ||
|
|
94fec57c9e | ||
|
|
345b125f6b | ||
|
|
248e91f982 | ||
|
|
32e8909e42 | ||
|
|
e8f02ac13e | ||
|
|
254cd6471b | ||
|
|
c2e7d0c2d6 | ||
|
|
0608d43d44 | ||
|
|
d67fc4a76d | ||
|
|
f37f54e1da | ||
|
|
1f4b49a21a | ||
|
|
7cccf672f5 | ||
|
|
c78eb07385 | ||
|
|
82f4c31651 | ||
|
|
af45ffc396 | ||
|
|
79173a68cc | ||
|
|
c60f33edeb | ||
|
|
bb3d351be8 | ||
|
|
2510c5dbb9 | ||
|
|
9ba4bd986d | ||
|
|
b03b06de6a | ||
|
|
6b0e0b9fff | ||
|
|
bfea51570f | ||
|
|
ac5feb4723 | ||
|
|
e32ed8deb5 | ||
|
|
46e82f0816 | ||
|
|
ea20f6836b | ||
|
|
634cc53405 | ||
|
|
410ed8914c | ||
|
|
78ed4d7f14 | ||
|
|
4fdc55e9b8 | ||
|
|
a60f5b8d7c | ||
|
|
dd9f4b2a33 | ||
|
|
b7d2b4a1ed | ||
|
|
4a6771a907 | ||
|
|
824f8e3d53 | ||
|
|
fda22ce80c | ||
|
|
1f99f7186b | ||
|
|
bb715fb206 | ||
|
|
2c5599c234 | ||
|
|
97efef3d40 | ||
|
|
2795a6cd52 | ||
|
|
94f13e44f9 | ||
|
|
fc7e748ac8 | ||
|
|
feb2f64f92 | ||
|
|
af14494637 | ||
|
|
35606d971d | ||
|
|
7d4aed60d8 | ||
|
|
f984cdd213 | ||
|
|
db26a5d683 | ||
|
|
413cce87c3 | ||
|
|
db0498b22c | ||
|
|
0e642ba21f | ||
|
|
1517f30abf | ||
|
|
3a9ffc6206 | ||
|
|
8c7caf45fa | ||
|
|
17dcde97aa | ||
|
|
a04f255146 | ||
|
|
8cd4ca97e6 | ||
|
|
8412ac29f2 | ||
|
|
cb6a5d9e1b | ||
|
|
5c977a348b | ||
|
|
0de5c094e5 | ||
|
|
d2386d62f1 | ||
|
|
ace6e2dd2c | ||
|
|
d563ac1259 | ||
|
|
f522307ee0 | ||
|
|
68a894edab | ||
|
|
d61f6c25e3 | ||
|
|
0822217480 | ||
|
|
9dc9a4645b | ||
|
|
096c432eff | ||
|
|
706a8b6d37 | ||
|
|
55323f1f54 | ||
|
|
b208532788 | ||
|
|
0464285ce1 | ||
|
|
7eb46ff555 | ||
|
|
cca8bb33ff | ||
|
|
55720aca54 | ||
|
|
822032de13 | ||
|
|
13d9dbfc62 | ||
|
|
484b3df122 | ||
|
|
7532c2eca5 | ||
|
|
9dc883dce9 | ||
|
|
d78663a528 | ||
|
|
385c671756 | ||
|
|
b58ec7359d | ||
|
|
8c701d4df5 | ||
|
|
70ccfcfd80 | ||
|
|
69e8ed19b8 | ||
|
|
b5680c5cb7 | ||
|
|
e1dcdc8257 | ||
|
|
e60dde03eb | ||
|
|
5528102f80 | ||
|
|
280ad0823f | ||
|
|
4f5eda0489 | ||
|
|
3d71b70503 | ||
|
|
2d8c528a6a | ||
|
|
c5c8a188c1 | ||
|
|
0ca48fa6a1 | ||
|
|
5a8c407528 | ||
|
|
c1e1c1e4cc | ||
|
|
5ef4f12c6e | ||
|
|
19548d3fac | ||
|
|
5ac872566a | ||
|
|
9965450f5b | ||
|
|
d0ad99d528 | ||
|
|
f7a56631c5 | ||
|
|
7f52cb1843 | ||
|
|
ecf4824f6b | ||
|
|
980439c646 | ||
|
|
0a3e3e80c6 | ||
|
|
ce9864a57b | ||
|
|
262c6d4324 | ||
|
|
88f7a3c7d6 | ||
|
|
69df7aa86d | ||
|
|
7e348465ac | ||
|
|
85772f7335 | ||
|
|
c9e84e8b38 | ||
|
|
cbab57be66 | ||
|
|
568906c96b | ||
|
|
d9c3583631 | ||
|
|
f703630914 | ||
|
|
be55f44a87 | ||
|
|
d12b0b145b | ||
|
|
a9005e7f1c | ||
|
|
965c2277fd | ||
|
|
a09c628f23 | ||
|
|
9b24b54215 | ||
|
|
92a7aee530 | ||
|
|
f52a04b4dc | ||
|
|
974df3e152 | ||
|
|
e4a0a41a1b | ||
|
|
f9564b2fc5 | ||
|
|
7e8f692b93 | ||
|
|
97f0eb5efd | ||
|
|
77ed27cfd1 | ||
|
|
ece51b2ef6 | ||
|
|
0f09b797ed | ||
|
|
0e4942e99c | ||
|
|
19cd1af3a4 | ||
|
|
824ebc7802 | ||
|
|
96cc9e51f4 | ||
|
|
3388103acf | ||
|
|
6032d2405e | ||
|
|
3068ccf8d7 | ||
|
|
bbfe61106c | ||
|
|
b4b06f324b | ||
|
|
8066776a80 | ||
|
|
54129e91a0 | ||
|
|
9a0bdf92f2 | ||
|
|
b997a3549e | ||
|
|
ddc9480d20 | ||
|
|
84957c6d0a | ||
|
|
ef678bcc36 | ||
|
|
88eb5211f7 | ||
|
|
331bea7a51 | ||
|
|
58b3af893c | ||
|
|
b44eb106a3 | ||
|
|
064ef32377 | ||
|
|
a8e92303fa | ||
|
|
17349a0840 | ||
|
|
a1c50f1031 | ||
|
|
5b30acc897 | ||
|
|
c2ad207b18 | ||
|
|
7dc695a18c | ||
|
|
1c58d99337 | ||
|
|
290dfe268d | ||
|
|
8b58be68a2 | ||
|
|
c62b8ab4af | ||
|
|
ae7bb20ed0 | ||
|
|
e323896d54 | ||
|
|
67cb4941bf | ||
|
|
599236f451 | ||
|
|
badbb2c768 | ||
|
|
5ab2736c92 | ||
|
|
1cef195715 | ||
|
|
d635318f73 | ||
|
|
8a7e16bf73 | ||
|
|
24d9827dae | ||
|
|
4d2b5d4642 | ||
|
|
54494b33be | ||
|
|
4efa2656f2 | ||
|
|
f14990efad | ||
|
|
231942384c | ||
|
|
a39b720935 | ||
|
|
d71fcb35ed | ||
|
|
39b4acd0eb | ||
|
|
2c72d0fbfa | ||
|
|
946240dda4 | ||
|
|
3b6396426e | ||
|
|
1756069330 | ||
|
|
d2b3e7201d | ||
|
|
2d4b049717 | ||
|
|
6e94215e7a | ||
|
|
863cea15fa | ||
|
|
19a9744c3f | ||
|
|
5ef84a5e41 | ||
|
|
c5116878f3 | ||
|
|
22c46afa04 | ||
|
|
8f1035ff97 | ||
|
|
c3ce68b512 | ||
|
|
8886213a2b | ||
|
|
fbe9057d82 | ||
|
|
ca9b27f895 | ||
|
|
2810ed79a1 | ||
|
|
cf31d1f358 | ||
|
|
c746e09b62 | ||
|
|
f050521fbd | ||
|
|
fbb7e38157 | ||
|
|
4bfc0ffc7a | ||
|
|
d6c9a856dc | ||
|
|
050cd1fa0f | ||
|
|
cf72017a5b | ||
|
|
ec92808ff4 | ||
|
|
7847bd1348 | ||
|
|
f2ae522c62 | ||
|
|
1f624b6d50 | ||
|
|
e7cce0c12b | ||
|
|
8afb25dbf0 | ||
|
|
213c6470a7 | ||
|
|
21c0d5a791 | ||
|
|
cfb42899c7 | ||
|
|
34f49bede5 | ||
|
|
7007e8bd03 | ||
|
|
9000af14f3 | ||
|
|
0ff8dafaf3 | ||
|
|
19d447c44c | ||
|
|
21cb44e709 | ||
|
|
f3659146bf | ||
|
|
1934ee7623 | ||
|
|
9a514d1b43 | ||
|
|
c5333faa03 | ||
|
|
49b6da2cf2 | ||
|
|
8cc0a6dbc3 | ||
|
|
ebe8a957f5 | ||
|
|
839143167b | ||
|
|
63f488082b | ||
|
|
378465b373 | ||
|
|
a47d2db99e | ||
|
|
12f4b35d71 | ||
|
|
c4d9dd09a3 | ||
|
|
2ba23ee7f7 | ||
|
|
1d4cdb8a3c |
79 changed files with 6627 additions and 4155 deletions
|
|
@ -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
27
.github/workflows/cljs-tests.yml
vendored
Normal 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
|
||||||
12
.github/workflows/graal-tests.yml
vendored
12
.github/workflows/graal-tests.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
459
CHANGELOG.md
459
CHANGELOG.md
|
|
@ -2,43 +2,452 @@ This project uses [**Break Versioning**](https://www.taoensso.com/break-versioni
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# `v1.0.0-beta3` (2024-04-23)
|
# `v1.2.1` (2025-12-16)
|
||||||
|
|
||||||
> **Dep/s**: [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-beta3) and [Telemere SLF4J provider](https://clojars.org/com.taoensso/slf4j-telemere/versions/1.0.0-beta3) are on Clojars.
|
## 📦 Dependencies
|
||||||
> **Versioning**: Telemere uses [Break Versioning](https://www.taoensso.com/break-versioning).
|
|
||||||
|
|
||||||
This is a **maintenance pre-release** intended to fix issues that have come up during the beta. See below for details, and please **report any unexpected problems** on [GitHub](https://github.com/taoensso/telemere/issues) or the [Slack channel](https://www.taoensso.com/telemere/slack), thank you! 🙏
|
Available on Clojars:
|
||||||
|
|
||||||
\- Peter Taoussanis
|
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
|
||||||
|
|
||||||
## Changes since `v1.0.0-beta1`
|
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
|
||||||
|
|
||||||
* d0a15bac [mod] Don't auto add OpenTelemetry handler
|
## Release notes
|
||||||
* 6d545dfc [mod] Move (simplify) OpenTelemetry ns
|
|
||||||
* d3c63e17 [mod] Rename `clojure.tools.logging` sys val
|
|
||||||
|
|
||||||
## Fixes since `v1.0.0-beta1`
|
This is a **hotfix release** to fix a regression in v1.2.0 that prevented errors from correctly appearing via the Timbre->Telemere appender.
|
||||||
|
|
||||||
* ffea1a30 [fix] Fix broken AOT support, add AOT tests
|
This should be a safe upgrade for users of v1.2.0, apologies for the trouble! - Peter Taoussanis
|
||||||
* e222297a [fix] SLF4J broken timestamps, add tests
|
|
||||||
|
|
||||||
## New since `v1.0.0-beta1`
|
|
||||||
|
|
||||||
* Handlers will now drain their signal queues on shutdown (configurable)
|
|
||||||
* Rate limiter performance improvements (via Encore)
|
|
||||||
* Misc improvements to docs
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# `v1.0.0-beta1` (2024-04-19)
|
# `v1.2.0` (2025-12-09)
|
||||||
|
|
||||||
> **Dep/s**: [Telemere](https://clojars.org/com.taoensso/telemere/versions/1.0.0-beta1) and [Telemere SLF4J provider](https://clojars.org/com.taoensso/slf4j-telemere/versions/1.0.0-beta1) are on Clojars.
|
## 📦 Dependencies
|
||||||
> **Versioning**: Telemere uses [Break Versioning](https://www.taoensso.com/break-versioning).
|
|
||||||
|
|
||||||
This is Telemere's first public pre-release and mostly intended for early testers and those that'd like to give feedback.
|
Available on Clojars:
|
||||||
|
|
||||||
While no significant changes are expected before the [planned v1.0 final release](https://www.taoensso.com/roadmap), you **probably don't want to use this in production** just yet.
|
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
|
||||||
|
|
||||||
**Please report any unexpected problems** and let me know if anything is unclear, inconvenient, etc. Now's the ideal time to get any last-minute changes in. Thank you! 🙏
|
This project uses [Break Versioning](https://www.taoensso.com/break-versioning).
|
||||||
|
|
||||||
\- Peter Taoussanis
|
## 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)
|
||||||
|
|
||||||
|
## Recent changes
|
||||||
|
|
||||||
|
### Since `v1.0.0-beta25` (2024-09-25)
|
||||||
|
|
||||||
|
* No API changes
|
||||||
|
|
||||||
|
### 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 `: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
|
||||||
|
|
||||||
|
### Since `v1.0.0-beta25` (2024-09-25)
|
||||||
|
|
||||||
|
* **\[new]** Add `:ctx+`, `:middleware+` signal options \[5a8c407] (RC1)
|
||||||
|
* **\[new]** OpenTelemetry handler: try print map vals as EDN \[c1e1c1e] (RC1)
|
||||||
|
* **\[new]** [#28] OpenTelemetry handler: support custom signal attrs \[5ef4f12] (RC1)
|
||||||
|
* **\[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
|
||||||
|
|
||||||
|
* \[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] [#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
|
||||||
|
|
||||||
|
### Since `v1.0.0-beta25` (2024-09-25)
|
||||||
|
|
||||||
|
* **\[fix]** `signal-opts`: allow map forms as intended \[f7a5663] (RC1)
|
||||||
|
* **\[fix]** `uncaught->error!` wasn't working (@benalbrecht) \[7f52cb1] (RC1)
|
||||||
|
|
||||||
|
### 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] [#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)
|
||||||
|
|
|
||||||
429
README.md
429
README.md
|
|
@ -1,216 +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>
|
||||||
[**Documentation**](#documentation) | [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). It handles **structured and traditional logging**, **tracing**, and **basic performance monitoring** with a simple unified API.
|
**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!"})
|
||||||
|
```
|
||||||
|
|
||||||
- `v1.0.0-beta3`: [release info](../../releases/tag/v1.0.0-beta3)
|
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**. It’s 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/), [clojure.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.
|
||||||
|
|
||||||
#### 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).
|
||||||
|
- 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 support for **system-level config** (JVM props, ENV vars, classpath resources).
|
- Telemere [compared](../../wiki/6-FAQ#how-does-telemere-compare-to-%CE%BClog) to [μ/log](https://github.com/BrunoBonacci/mulog) (structured micro-logging library)
|
||||||
- Expressive **per-call** and **per-handler** filtering at both **runtime** and **compile-time**.
|
|
||||||
- Filter by namespace and id pattern, level, **level by namespace pattern**, etc.
|
|
||||||
- **Sampling**, **rate-limiting**, and **back-pressure monitoring**.
|
|
||||||
- **Fully configurable a/sync dispatch** (blocking, dropping, sliding, etc.).
|
|
||||||
|
|
||||||
## Video demo
|
## Videos
|
||||||
|
|
||||||
See for intro and usage:
|
### Lightning intro (7 mins):
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
### 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])
|
|
||||||
|
|
||||||
;; Start simple
|
|
||||||
(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
|
|
||||||
(let [x "constructed"] (t/log! :info ["Here's a" x "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 available to `:data` and message, 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")
|
|
||||||
|
|
||||||
;;; A quick taste of filtering...
|
|
||||||
|
|
||||||
(t/set-ns-filter! {:deny "taoensso.*" :allow "taoensso.sente.*"}) ; Set namespace filter
|
|
||||||
(t/set-id-filter! {:allow #{::my-particular-id "my-app/*"}}) ; Set id filter
|
|
||||||
|
|
||||||
(t/set-min-level! :warn) ; Set minimum level
|
|
||||||
(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 [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
|
||||||
|
|
||||||
### Signal creators
|
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).
|
||||||
|
|
||||||
| Name | Signal kind | Main arg | Optional arg | Returns |
|
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:
|
||||||
| :---------------------------------------------------------------------------------------------------------- | :---------- | :------- | :------------- | :--------------------------- |
|
|
||||||
| [`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 |
|
|
||||||
|
|
||||||
### Signal filters
|
| Name | Args | Returns |
|
||||||
|
| :---------------------------------------------------------------------------------------------------------- | :------------------------- | :--------------------------- |
|
||||||
| Global | Dynamic | Filters by |
|
| [`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 |
|
||||||
| [`set-kind-filter!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-kind-filter!) | [`with-kind-filter`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-kind-filter) | Signal kind (`:log`, `:event`, etc.) |
|
| [`trace!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#trace!) | `[opts]` or `[?id run]` | Form result |
|
||||||
| [`set-ns-filter!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-ns-filter!) | [`with-ns-filter`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-ns-filter) | Signal namespace |
|
| [`spy!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#spy!) | `[opts]` or `[?level run]` | Form result |
|
||||||
| [`set-id-filter!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-id-filter!) | [`with-id-filter`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-id-filter) | Signal id |
|
| [`error!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error!) | `[opts]` or `[?id error]` | Given error |
|
||||||
| [`set-min-level`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-min-level) | [`with-min-level`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-min-level) | Signal level (minimum can be specified by kind and/or ns) |
|
| [`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:
|
||||||
|
|
||||||
| Var | Help with |
|
| Var | Help with |
|
||||||
| :-------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
|
| :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
|
||||||
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | List of signal creators |
|
| [`help:signal-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 for signal creators |
|
| [`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 map content |
|
| [`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:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
| [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation |
|
||||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
| [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management |
|
||||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
| [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options |
|
||||||
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
|
| [`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 |
|
||||||
|
|
||||||
### Example handler output
|
## Performance
|
||||||
|
|
||||||
```clojure
|
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.
|
||||||
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message") =>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Clj console handler
|
Signal call benchmarks (per thread):
|
||||||
|
|
||||||
String output:
|
| Compile-time filtering? | Runtime filtering? | Profile? | Trace? | nsecs / call |
|
||||||
|
| :---------------------: | :----------------: | :------: | :----: | -----------: |
|
||||||
|
| ✓ (elide) | - | - | - | 0 |
|
||||||
|
| - | ✓ | - | - | 350 |
|
||||||
|
| - | ✓ | ✓ | - | 450 |
|
||||||
|
| - | ✓ | ✓ | ✓ | 1000 |
|
||||||
|
|
||||||
```
|
- Nanoseconds per signal call ~ **milliseconds per 1e6 calls**
|
||||||
2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message
|
- 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)
|
||||||
data: {:x1 :x2}
|
- Benched on a 2020 Macbook Pro M1, running Clojure v1.12 and OpenJDK v22
|
||||||
```
|
|
||||||
|
|
||||||
#### Cljs console handler
|
### Performance philosophy
|
||||||
|
|
||||||
Chrome console:
|
Telemere is optimized for *real-world* performance. This means **prioritizing flexibility** and realistic usage over synthetic micro-benchmarks.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-cljs-console.png" alt="Default ClojureScript console handler output" width="640"/>
|
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.
|
||||||
|
|
||||||
#### Cljs raw console handler
|
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.
|
||||||
|
|
||||||
Chrome console, with [cljs-devtools](https://github.com/binaryage/cljs-devtools):
|
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.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-cljs-console-raw.png" alt="Raw ClojureScript console handler output" width="640"/>
|
See [here](../../wiki/7-Tips) for detailed tips on real-world usage.
|
||||||
|
|
||||||
#### Clj file handler
|
## Included handlers
|
||||||
|
|
||||||
MacOS terminal:
|
See ✅ links below for **features and usage**,
|
||||||
|
See ❤️ links below to **vote on future handlers**:
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-clj-file.png" alt="Default Clojure file handler output" width="640"/>
|
| 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
|
## Documentation
|
||||||
|
|
||||||
- [Wiki][GitHub wiki] (getting started, usage, etc.)
|
- [Wiki][GitHub wiki] (getting started, usage, etc.)
|
||||||
- API reference: [cljdoc][cljdoc docs] or [Codox][Codox docs]
|
- API reference via [cljdoc][cljdoc]
|
||||||
- Support: [Slack channel][] or [GitHub issues][]
|
- Extensive [internal help](#internal-help) (no need to leave your IDE)
|
||||||
|
- Support via [Slack][] or [GitHub issues][]
|
||||||
## Observability tips
|
- [General observability tips](../../wiki/7-Tips) (advice on building and maintaining observable Clojure/Script systems, and getting the most out of Telemere)
|
||||||
|
|
||||||
See [here](../../wiki/7-Tips) for general advice re: building and maintaining observable Clojure/Script systems.
|
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
Telemere is **highly optimized** and offers terrific performance at any scale:
|
|
||||||
|
|
||||||
| Compile-time filtering? | Runtime filtering? | Time? | Trace? | nsecs
|
|
||||||
| :-: | :-: | :-: | :-: | --:
|
|
||||||
| ✓ (elide) | - | - | - | 0
|
|
||||||
| - | ✓ | - | - | 200
|
|
||||||
| - | ✓ | ✓ | - | 280
|
|
||||||
| - | ✓ | ✓ | ✓ | 650
|
|
||||||
|
|
||||||
Measurements:
|
|
||||||
|
|
||||||
- Are **~nanoseconds per signal call** (= milliseconds per 1e6 calls)
|
|
||||||
- Exclude handler runtime (which depends on handler/s, is usually async)
|
|
||||||
- Taken on a 2020 Macbook Pro M1, running OpenJDK 21
|
|
||||||
|
|
||||||
**Tip**: Telemere offers extensive per-call and per-handler **filtering**, **sampling**, and **rate-limiting**. Use these to ensure that you're not capturing useless/low-value information in production. See [here](../../wiki/7-Tips) for more tips!
|
|
||||||
|
|
||||||
## 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 © 2023-2024 [Peter Taoussanis][].
|
Copyright © 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 -->
|
||||||
|
|
@ -218,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
|
||||||
|
|
|
||||||
384
examples.cljc
384
examples.cljc
|
|
@ -1,205 +1,321 @@
|
||||||
(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"
|
||||||
|
|
||||||
;; Start simple
|
(require '[taoensso.telemere :as tel])
|
||||||
(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
|
;; No config needed for typical use cases!!
|
||||||
(let [x "constructed"] (t/log! :info ["Here's a" x "message!"]))
|
;; Signals print to console by default for both Clj and Cljs
|
||||||
|
|
||||||
;; Attach an id
|
;; Traditional style logging (data formatted into message string):
|
||||||
(t/log! {:level :info, :id ::my-id} "This signal has an id")
|
(tel/log! {:level :info, :msg (str "User " 1234 " logged in!")})
|
||||||
|
|
||||||
;; Attach arb user data
|
;; Modern/structured style logging (explicit id and data)
|
||||||
(t/log! {:level :info, :data {:x :y}} "This signal has structured data")
|
(tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}})
|
||||||
|
|
||||||
;; Capture for debug/testing
|
;; Mixed style (explicit id and data, with message string)
|
||||||
(t/with-signal (t/log! "This will be captured"))
|
(tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}, :msg "User logged in!"})
|
||||||
;; => {:keys [location level id data msg_ ...]}
|
|
||||||
|
|
||||||
;; `:let` bindings available to `:data` and message, only paid for
|
;; Trace (can interop with OpenTelemetry)
|
||||||
;; when allowed by minimum level and other filtering criteria
|
;; Tracks form runtime, return value, and (nested) parent tree
|
||||||
(t/log!
|
(tel/trace! {:id ::my-id :data {...}}
|
||||||
{:level :info
|
(do-some-work))
|
||||||
: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
|
;; Check resulting signal content for debug/tests
|
||||||
(t/log!
|
(tel/with-signal (tel/log! {...})) ; => {:keys [ns level id data msg_ ...]}
|
||||||
{:sample-rate 0.5
|
|
||||||
:rate-limit {"1 per sec" [1 1000]}}
|
|
||||||
"This signal will be sampled and rate limited")
|
|
||||||
|
|
||||||
;;; A quick taste of filtering...
|
;; 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
|
||||||
|
|
||||||
(t/set-ns-filter! {:deny "taoensso.*" :allow "taoensso.sente.*"}) ; Set namespace filter
|
:do (inc-my-metric!)
|
||||||
(t/set-id-filter! {:allow #{::my-particular-id "my-app/*"}}) ; Set id filter
|
:let
|
||||||
|
[diagnostics (my-expensive-diagnostics)
|
||||||
|
formatted (my-expensive-format diagnostics)]
|
||||||
|
|
||||||
(t/set-min-level! :warn) ; Set minimum level
|
:data
|
||||||
(t/set-min-level! :log :debug) ; Set minimul level for `log!` signals
|
{:diagnostics diagnostics
|
||||||
|
:formatted formatted
|
||||||
|
:local-state *my-dynamic-context*}}
|
||||||
|
|
||||||
;; Set minimum level for `event!` signals originating in the "taoensso.sente.*" ns
|
;; Message string or vector to join as string
|
||||||
(t/set-min-level! :event "taoensso.sente.*" :warn)
|
["Something interesting happened!" formatted])
|
||||||
|
)
|
||||||
|
|
||||||
;;; Example handler output
|
;; Set minimum level
|
||||||
|
(tel/set-min-level! :warn) ; For all signals
|
||||||
|
(tel/set-min-level! :log :debug) ; For `log!` signals specifically
|
||||||
|
|
||||||
(t/log! {:id ::my-id, :data {:x1 :x2}} "My message") ; =>
|
;; Set id and namespace filters
|
||||||
;; 2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message
|
(tel/set-id-filter! {:allow #{::my-particular-id "my-app/*"}})
|
||||||
;; data: {:x1 :x2}
|
(tel/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"})
|
||||||
|
|
||||||
;;;; Docstring snippets
|
;; 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/with-signal (t/event! ::my-id))
|
;; Set minimum level for `log!` signals for particular ns pattern
|
||||||
(t/with-signal (t/event! ::my-id :warn))
|
(tel/set-min-level! :log "taoensso.sente.*" :warn)
|
||||||
(t/with-signal
|
|
||||||
(t/event! ::my-id
|
;; 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
|
||||||
|
|
||||||
|
;; 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)}))
|
||||||
|
|
||||||
|
;;;; 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 ...]}
|
||||||
|
|
||||||
;; Deny all signals in matching namespaces
|
;; Disallow all signals in matching namespaces
|
||||||
(t/set-ns-filter! {:deny "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 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
|
||||||
{:format-signal-fn (taoensso.telemere.utils/format-signal->edn-fn)}))
|
{: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 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]))
|
||||||
(def my-handler
|
(def my-handler
|
||||||
(t/handler:console
|
(tel/handler:console
|
||||||
{:format-signal-fn
|
{:output-fn
|
||||||
(taoensso.telemere.utils/format-signal->json-fn
|
(tel/pr-signal-fn
|
||||||
{:pr-json-fn jsonista.core/write-value-as-string})}))
|
{:pr-fn
|
||||||
|
#?(:cljs :json ; Use js/JSON.stringify
|
||||||
|
: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
|
||||||
|
(tel/remove-handler! :defaultel/console)
|
||||||
|
|
||||||
|
;; Register our custom console handler
|
||||||
|
(tel/add-handler! :my-handler my-handler
|
||||||
|
;; Lots of options here for filtering, etc.
|
||||||
|
;; See `help:handler-dispatch-options` docstring!
|
||||||
|
{})
|
||||||
|
|
||||||
|
;; NB make sure to always stop handlers at the end of your
|
||||||
|
;; `-main` or shutdown procedure
|
||||||
|
(tel/call-on-shutdown!
|
||||||
|
(fn [] (tel/stop-handlers!)))
|
||||||
|
|
||||||
|
;; See `tel/help:handlers` docstring for more
|
||||||
|
|
||||||
;;; Writing handlers
|
;;; Writing handlers
|
||||||
|
|
||||||
(defn handler:my-handler ; Note naming convention
|
;; Handlers are just fns of 2 arities
|
||||||
"Returns a (fn handler [signal] that:
|
|
||||||
- Does something.
|
(defn my-basic-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
|
||||||
|
;; handy to define a handler constructor
|
||||||
|
|
||||||
|
(defn handler:my-fancy-handler ; Note constructor naming convention
|
||||||
|
"Needs `some-lib`, Ref. <https://github.com/example/some-lib>.
|
||||||
|
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Does something useful with the signal!
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
`:option1` - Description
|
`:option1` - Option description
|
||||||
`:option2` - Description"
|
`:option2` - Option description
|
||||||
|
|
||||||
([] (handler:my-handler nil)) ; Use default opts
|
Tips:
|
||||||
|
- Tip 1
|
||||||
|
- Tip 2"
|
||||||
|
|
||||||
|
([] (handler:my-fancy-handler nil)) ; Use default opts (iff defaults viable)
|
||||||
([{:as constructor-opts}]
|
([{:as constructor-opts}]
|
||||||
|
|
||||||
;; Do expensive prep outside returned handler fn whenever possible -
|
;; Do option validation and other prep here, i.e. try to keep
|
||||||
;; i.e. at (one-off) construction time rather than handling time.
|
;; expensive work outside handler function when possible!
|
||||||
(let []
|
|
||||||
|
|
||||||
(fn a-handler:my-handler ; Note naming convention
|
(let [handler-fn ; Fn of exactly 2 arities (1 and 0)
|
||||||
|
(fn a-handler:my-fancy-handler ; Note fn naming convention
|
||||||
|
|
||||||
;; Shutdown arity - called by Telemere exactly once when the handler is
|
([signal] ; Arity-1 called when handling a signal
|
||||||
;; to be shut down. This is your opportunity to finalize/free resources, etc.
|
;; Do something useful with the given signal (write to
|
||||||
([])
|
;; console/file/queue/db, etc.). Return value is ignored.
|
||||||
|
)
|
||||||
|
|
||||||
;; Main arity - called by Telemere whenever the handler should handle the
|
([] ; Arity-0 called when stopping the handler
|
||||||
;; given signal. Never called after shutdown.
|
;; Flush buffers, close files, etc. May just noop.
|
||||||
([signal]
|
;; Return value is ignored.
|
||||||
;; TODO Do something with given signal
|
))]
|
||||||
)))))
|
|
||||||
|
;; (Advanced, optional) You can use metadata to provide default
|
||||||
|
;; handler dispatch options (see `help:handler-dispatch-options`)
|
||||||
|
|
||||||
|
(with-meta handler-fn
|
||||||
|
{:dispatch-opts
|
||||||
|
{:min-level :info
|
||||||
|
:limit
|
||||||
|
[[1 1000] ; Max 1 signal per second
|
||||||
|
[10 60000] ; Max 10 signals per minute
|
||||||
|
]}}))))
|
||||||
|
|
||||||
;;; 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)]
|
||||||
|
|
@ -212,8 +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
|
||||||
|
|
||||||
|
(tel/with-signal
|
||||||
|
(tel/event! ::my-id
|
||||||
|
{:my-data-for-xfn "foo"
|
||||||
|
:my-data-for-handler "bar"}))
|
||||||
|
|
||||||
|
;; %>
|
||||||
|
;; {;; App-level kvs included inline (assoc'd to signal root)
|
||||||
|
;; :my-data-for-xfn "foo"
|
||||||
|
;; :my-data-for-handler "bar"
|
||||||
|
;; :kvs ; And also collected together under ":kvs" key
|
||||||
|
;; {:my-data-for-xfn "foo"
|
||||||
|
;; :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]
|
||||||
|
|
||||||
|
)
|
||||||
|
|
|
||||||
1
handlers/consoles.cljc
Symbolic link
1
handlers/consoles.cljc
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../main/src/taoensso/telemere/consoles.cljc
|
||||||
1
handlers/files.clj
Symbolic link
1
handlers/files.clj
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../main/src/taoensso/telemere/files.clj
|
||||||
1
handlers/open_telemetry.clj
Symbolic link
1
handlers/open_telemetry.clj
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../main/src/taoensso/telemere/open_telemetry.clj
|
||||||
1
handlers/postal.clj
Symbolic link
1
handlers/postal.clj
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../main/src/taoensso/telemere/postal.clj
|
||||||
1
handlers/slack.clj
Symbolic link
1
handlers/slack.clj
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../main/src/taoensso/telemere/slack.clj
|
||||||
1
handlers/sockets.clj
Symbolic link
1
handlers/sockets.clj
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../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
5
install.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd main; lein install; cd ..;
|
||||||
|
cd slf4j; lein install; cd ..;
|
||||||
|
|
||||||
0
.gitignore → main/.gitignore
vendored
0
.gitignore → main/.gitignore
vendored
8
main/jaeger.sh
Executable file
8
main/jaeger.sh
Executable 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"
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
(defproject com.taoensso/telemere "1.0.0-beta3"
|
(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.104.1"]]
|
[[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.2"]]}
|
[org.clojure/clojure "1.11.4"]]}
|
||||||
:c1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha9"]]}
|
:c1.12 {:dependencies [[org.clojure/clojure "1.12.3"]]}
|
||||||
:c1.11 {:dependencies [[org.clojure/clojure "1.11.2"]]}
|
:c1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]}
|
||||||
:c1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}
|
:c1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]}
|
||||||
|
|
||||||
:graal-tests
|
:graal-tests
|
||||||
{:source-paths ["test"]
|
{:source-paths ["test"]
|
||||||
|
|
@ -26,10 +28,12 @@
|
||||||
:aot [taoensso.graal-tests]
|
:aot [taoensso.graal-tests]
|
||||||
:uberjar-name "graal-tests.jar"
|
:uberjar-name "graal-tests.jar"
|
||||||
:dependencies
|
:dependencies
|
||||||
[[org.clojure/clojure "1.11.2"]
|
[[org.clojure/clojure "1.11.4"]
|
||||||
[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,25 +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.13"]
|
[org.slf4j/slf4j-api "2.0.17"]
|
||||||
[com.taoensso/slf4j-telemere "1.0.0-beta3"]
|
[com.taoensso/telemere-slf4j "1.2.1"]
|
||||||
;; [org.slf4j/slf4j-simple "2.0.13"]
|
#_[org.slf4j/slf4j-simple "2.0.16"]
|
||||||
;; [org.slf4j/slf4j-nop "2.0.13"]
|
#_[org.slf4j/slf4j-nop "2.0.16"]
|
||||||
[io.opentelemetry/opentelemetry-api "1.37.0"]
|
#_[io.github.paintparty/bling "0.4.2"]
|
||||||
[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.37.0"]
|
|
||||||
[io.opentelemetry/opentelemetry-exporter-otlp "1.37.0"]]
|
;;; For optional handlers
|
||||||
|
[io.opentelemetry/opentelemetry-api "1.57.0"]
|
||||||
|
[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.57.0"]
|
||||||
|
[io.opentelemetry/opentelemetry-exporter-otlp "1.57.0"]
|
||||||
|
#_[io.opentelemetry/opentelemetry-exporters-jaeger "0.9.1"]
|
||||||
|
[metosin/jsonista "0.3.13"]
|
||||||
|
[com.draines/postal "2.0.5"]
|
||||||
|
[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"]}
|
||||||
|
|
@ -85,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"]]})
|
||||||
40
main/resources/docs/catch-to-error!.txt
Normal file
40
main/resources/docs/catch-to-error!.txt
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
ALWAYS (unconditionally) executes given `run` form and:
|
||||||
|
|
||||||
|
Default kind: `: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:
|
||||||
|
|
||||||
|
(catch->error! (/ 1 0)) ; %> {:kind :error, :level :error, :error <caught> ...}
|
||||||
|
(catch->error! ::my-id (/ 1 0)) ; %> {... :id ::my-id ...}
|
||||||
|
(catch->error!
|
||||||
|
{:let [x "x"] ; Available to `:data` and `:msg`
|
||||||
|
:data {:x x}
|
||||||
|
:msg ["My msg:" x]
|
||||||
|
:catch-val "Return value iff form throws"}
|
||||||
|
|
||||||
|
(/ 1 0)) ; %> {... :data {x "x"}, :msg_ "My msg: x" ...}
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
|
||||||
|
- Test using `with-signal`: (with-signal (catch->error! ...)).
|
||||||
|
- Supports the same options [2] as other signals [1].
|
||||||
|
|
||||||
|
- Useful for preventing errors from going unnoticed in futures, callbacks,
|
||||||
|
agent actions, etc.!: (future (catch->error ::my-future (do-something)))
|
||||||
|
|
||||||
|
See also `error!`.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
[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.)
|
||||||
60
main/resources/docs/environmental-config.txt
Normal file
60
main/resources/docs/environmental-config.txt
Normal 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.
|
||||||
|
|
@ -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,10 +24,9 @@ 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!`, ...)
|
||||||
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
||||||
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
||||||
[4] See `help:signal-flow` - (filters, handling, etc.)
|
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -29,8 +31,8 @@ Tips:
|
||||||
Mnemonic: the arg that's typically larger is *always* in the rightmost
|
Mnemonic: the arg that's typically larger is *always* in the rightmost
|
||||||
position, and for `event!` that's the `level-or-opts` arg.
|
position, and for `event!` that's the `level-or-opts` arg.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
|
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
|
||||||
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
||||||
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
||||||
[4] See `help:signal-flow` - (filters, handling, etc.)
|
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -29,8 +31,8 @@ Tips:
|
||||||
- `msg` arg may be a string, or vector of strings to join with `\space`.
|
- `msg` arg may be a string, or vector of strings to join with `\space`.
|
||||||
- See also `msg-splice`, `msg-skip` utils.
|
- See also `msg-splice`, `msg-skip` utils.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
|
[1] See `help:signal-creators` - (`signal!`, `log!`, `event!`, ...)
|
||||||
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
||||||
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
||||||
[4] See `help:signal-flow` - (filters, handling, etc.)
|
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
|
||||||
30
main/resources/docs/signal!.txt
Normal file
30
main/resources/docs/signal!.txt
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
Low-level "generic" signal creator for creating signals of any "kind".
|
||||||
|
Takes a single map of options [2] with compile-time keys.
|
||||||
|
|
||||||
|
Default kind: `:generic` (feel free to change!)
|
||||||
|
Default level: `:info`
|
||||||
|
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
|
||||||
|
dispatches it to registered handlers for processing (e.g. writing to
|
||||||
|
console/file/queue/db, etc.).
|
||||||
|
|
||||||
|
Generic signals are fairly low-level and useful mostly for library authors or
|
||||||
|
advanced users writing their own wrapper macros. NB see `keep-callsite` for
|
||||||
|
preserving callsite coords when wrapping Telemere macros like `signal!`.
|
||||||
|
|
||||||
|
Regular users will typically prefer one of the higher-level signal creators
|
||||||
|
optimized for ease-of-use in common cases [1].
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
|
||||||
|
- Test using `with-signal`: (with-signal (signal! ...)).
|
||||||
|
- Supports the same options [2] as other signals [1].
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
[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.)
|
||||||
41
main/resources/docs/signal-content.txt
Normal file
41
main/resources/docs/signal-content.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
Telemere signals are maps with {:keys [inst id ns level data msg_ ...]},
|
||||||
|
though they can be modified by call and/or handler transform (xfns).
|
||||||
|
|
||||||
|
Default signal keys:
|
||||||
|
|
||||||
|
`:schema` ------ Int version of signal schema (current: 1)
|
||||||
|
`:inst` -------- Platform instant [1] when signal was created, monotonicity depends on system clock
|
||||||
|
`:ns` ---------- ?str namespace of signal callsite
|
||||||
|
`:coords` ------ ?[line column] of signal callsite
|
||||||
|
|
||||||
|
`: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
|
||||||
|
`:error` ------- Arb app-level platform ?error [2] given to signal creator
|
||||||
|
|
||||||
|
`:run-form` ---- Unevaluated ?form given to signal creator as `:run`
|
||||||
|
`:run-val` ----- Successful return ?val of `:run` ?form
|
||||||
|
`:run-nsecs` --- ?int nanosecs runtime of `:run` ?form
|
||||||
|
`:end-inst` ---- Platform ?instant [1] when `:run` ?form completed
|
||||||
|
|
||||||
|
`: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
|
||||||
|
`:ctx` --------- ?val of `*ctx*` (arb app-level state) when signal was created
|
||||||
|
|
||||||
|
`:host` -------- (Clj only) {:keys [name ip]} info for network host
|
||||||
|
`:thread` ------ (Clj only) {:keys [name id group]} info for thread that created signal
|
||||||
|
|
||||||
|
`: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
|
||||||
|
in handler output, so a great way to provide custom data/opts for use
|
||||||
|
(only) by custom transforms/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`
|
||||||
|
|
@ -11,22 +11,23 @@ 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:
|
||||||
|
|
||||||
`log!` ---------- [message + opts/level] => true iff signal was created (allowed)
|
`log!` ------------- ?level + msg => nil
|
||||||
`event!` -------- [id + opts/level] => true iff signal was created (allowed)
|
`event!` ----------- id + ?level => nil
|
||||||
`error!` -------- [error + opts/id ] => given error (unconditional)
|
`trace!` ----------- ?id + run => run result (value or throw)
|
||||||
`trace!` -------- [form + opts/id ] => form result (value/throw) (unconditional)
|
`spy!` ------------- ?level + run => run result (value or throw)
|
||||||
`spy!` ---------- [form + opts/level] => form result (value/throw) (unconditional)
|
`error!` ----------- ?id + error => given error
|
||||||
`catch->error!` - [error + opts/id ] => 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.
|
||||||
- `signal!` is the generic creator, and is used by all the others.
|
- `signal!` is the generic creator, and is used by all the others.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
[2] See `help:signal-options` - {:keys [kind level id data ...]}
|
||||||
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
[3] See `help:signal-content` - {:keys [kind level id data ...]}
|
||||||
[4] See `help:signal-flow` - (filters, handling, etc.)
|
[4] See `help:signal-filters` - (by ns/kind/id/level, sampling, etc.)
|
||||||
49
main/resources/docs/signal-options.txt
Normal file
49
main/resources/docs/signal-options.txt
Normal 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!
|
||||||
84
main/resources/docs/spy!.txt
Normal file
84
main/resources/docs/spy!.txt
Normal 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.)
|
||||||
88
main/resources/docs/trace!.txt
Normal file
88
main/resources/docs/trace!.txt
Normal 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.)
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{;;:lein true
|
{;;:lein true
|
||||||
:source-paths ["src" "test"]
|
:source-paths ["src" "test"]
|
||||||
:dependencies
|
:dependencies
|
||||||
[[com.taoensso/encore "3.104.1"]
|
[[com.taoensso/encore "3.112.0"]
|
||||||
[cider/cider-nrepl "0.47.0"]
|
[cider/cider-nrepl "0.47.0"]
|
||||||
[binaryage/devtools "1.0.7"]]
|
[binaryage/devtools "1.0.7"]]
|
||||||
|
|
||||||
557
main/src/taoensso/telemere.cljc
Normal file
557
main/src/taoensso/telemere.cljc
Normal 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))
|
||||||
129
main/src/taoensso/telemere/consoles.cljc
Normal file
129
main/src/taoensso/telemere/consoles.cljc
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
(ns ^:no-doc taoensso.telemere.consoles
|
||||||
|
"Telemere -> console handlers."
|
||||||
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.telemere.utils :as utils]))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(require '[taoensso.telemere :as tel])
|
||||||
|
(remove-ns (symbol (str *ns*)))
|
||||||
|
(:api (enc/interns-overview)))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn ^:public handler:console
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Writes the signal as a string to specified stream.
|
||||||
|
|
||||||
|
A general-purpose `println`-style handler that's well suited for outputting
|
||||||
|
signals as human or machine-readable (edn, JSON) strings.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
|
||||||
|
`:stream` ---- `java.io.writer`
|
||||||
|
Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise."
|
||||||
|
|
||||||
|
([] (handler:console nil))
|
||||||
|
([{:keys [stream output-fn]
|
||||||
|
:or
|
||||||
|
{stream :auto
|
||||||
|
output-fn (utils/format-signal-fn)}}]
|
||||||
|
|
||||||
|
(let [error-signal? utils/error-signal?]
|
||||||
|
|
||||||
|
(fn a-handler:console
|
||||||
|
([ ]) ; Stop => noop
|
||||||
|
([signal]
|
||||||
|
(let [^java.io.Writer stream
|
||||||
|
(case stream
|
||||||
|
(:out :*out*) *out*
|
||||||
|
(:err :*err*) *err*
|
||||||
|
:auto (if (error-signal? signal) *err* *out*)
|
||||||
|
stream)]
|
||||||
|
|
||||||
|
(when-let [output (output-fn signal)]
|
||||||
|
(.write stream (str output))
|
||||||
|
(.flush stream))))))))
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
(defn ^:public handler:console
|
||||||
|
"Alpha, subject to change.
|
||||||
|
If `js/console` exists, returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Writes the signal as a string to JavaScript console.
|
||||||
|
|
||||||
|
A general-purpose `println`-style handler that's well suited for outputting
|
||||||
|
signals as human or machine-readable (edn, JSON) strings.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`"
|
||||||
|
|
||||||
|
([] (handler:console nil))
|
||||||
|
([{:keys [output-fn]
|
||||||
|
:or {output-fn (utils/format-signal-fn)}}]
|
||||||
|
|
||||||
|
(when (exists? js/console)
|
||||||
|
(let [js-console-logger utils/js-console-logger]
|
||||||
|
|
||||||
|
(fn a-handler:console
|
||||||
|
([ ]) ; Stop => noop
|
||||||
|
([signal]
|
||||||
|
(when-let [output (output-fn signal)]
|
||||||
|
(let [logger (js-console-logger (get signal :level))]
|
||||||
|
(.call logger logger (str output)))))))))))
|
||||||
|
|
||||||
|
#?(:cljs
|
||||||
|
(defn- logger-fn [logger]
|
||||||
|
;; (fn [& xs] (.apply logger logger (into-array xs)))
|
||||||
|
(fn
|
||||||
|
([x1 ] (.call logger logger x1))
|
||||||
|
([x1 x2 ] (.call logger logger x1 x2))
|
||||||
|
([x1 x2 x3 ] (.call logger logger x1 x2 x3))
|
||||||
|
([x1 x2 x3 & more] (apply logger x1 x2 x3 more)))))
|
||||||
|
|
||||||
|
#?(:cljs
|
||||||
|
(defn ^:public handler:console-raw
|
||||||
|
"Alpha, subject to change.
|
||||||
|
If `js/console` exists, returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Writes the raw signal to JavaScript console.
|
||||||
|
|
||||||
|
Intended for use with browser formatting tools like `binaryage/devtools`,
|
||||||
|
Ref. <https://github.com/binaryage/cljs-devtools>.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:preamble-fn` ----- (fn [signal]) => string, see [1].
|
||||||
|
`:format-nsecs-fn` - (fn [nanosecs]) => string.
|
||||||
|
|
||||||
|
[1] `taoensso.telemere.utils/signal-preamble-fn`, etc."
|
||||||
|
|
||||||
|
([] (handler:console-raw nil))
|
||||||
|
([{:keys [preamble-fn format-nsecs-fn] :as opts
|
||||||
|
:or
|
||||||
|
{preamble-fn (utils/signal-preamble-fn)
|
||||||
|
format-nsecs-fn (utils/format-nsecs-fn)}}]
|
||||||
|
|
||||||
|
(when (and (exists? js/console) (exists? js/console.group))
|
||||||
|
(let [js-console-logger utils/js-console-logger
|
||||||
|
content-fn ; (fn [signal append-fn val-fn])
|
||||||
|
(utils/signal-content-fn
|
||||||
|
{:format-nsecs-fn format-nsecs-fn
|
||||||
|
:format-error-fn nil
|
||||||
|
:raw-error? true})]
|
||||||
|
|
||||||
|
(fn a-handler:console-raw
|
||||||
|
([ ]) ; Stop => noop
|
||||||
|
([signal]
|
||||||
|
(let [{:keys [level error]} signal
|
||||||
|
logger (js-console-logger level)]
|
||||||
|
|
||||||
|
;; Unfortunately groups have no level
|
||||||
|
(.group js/console (preamble-fn signal))
|
||||||
|
(content-fn signal (logger-fn logger) identity)
|
||||||
|
|
||||||
|
(when-let [stack (and error (.-stack (truss/ex-root error)))]
|
||||||
|
(.call logger logger stack))
|
||||||
|
|
||||||
|
(.groupEnd js/console)))))))))
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
(ns ^:no-doc taoensso.telemere.file-handler
|
(ns ^:no-doc taoensso.telemere.files
|
||||||
"Private ns, implementation detail.
|
"Telemere -> file handler."
|
||||||
Core archiving file handler."
|
|
||||||
(: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
|
||||||
(remove-ns 'taoensso.telemere.file-handler)
|
(require '[taoensso.telemere :as tel])
|
||||||
|
(remove-ns (symbol (str *ns*)))
|
||||||
(:api (enc/interns-overview)))
|
(:api (enc/interns-overview)))
|
||||||
|
|
||||||
;;;; Implementation
|
;;;; Implementation
|
||||||
|
|
@ -22,7 +23,7 @@
|
||||||
stream-out (java.io.FileOutputStream. file-out)
|
stream-out (java.io.FileOutputStream. file-out)
|
||||||
gz-out (java.util.zip.GZIPOutputStream. stream-out 2048 false)]
|
gz-out (java.util.zip.GZIPOutputStream. stream-out 2048 false)]
|
||||||
|
|
||||||
(let [read-buffer (byte-array 4096)]
|
(let [read-buffer (byte-array (.length file-in))]
|
||||||
(loop []
|
(loop []
|
||||||
(let [bytes-read (.read stream-in read-buffer)]
|
(let [bytes-read (.read stream-in read-buffer)]
|
||||||
(when-not (== -1 bytes-read)
|
(when-not (== -1 bytes-read)
|
||||||
|
|
@ -70,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*)))))
|
||||||
|
|
@ -80,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?]
|
||||||
|
|
@ -135,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`
|
||||||
|
|
||||||
|
|
@ -167,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
|
||||||
|
|
@ -233,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)
|
||||||
|
|
||||||
|
|
@ -267,39 +267,41 @@
|
||||||
(defn ^:public handler:file
|
(defn ^:public handler:file
|
||||||
"Experimental, subject to change.
|
"Experimental, subject to change.
|
||||||
|
|
||||||
Returns a (fn handler [signal]) that:
|
Returns a signal handler that:
|
||||||
- Takes a Telemere signal.
|
- Takes a Telemere signal (map).
|
||||||
- Writes a formatted signal string to file.
|
- Writes (appends) the signal as a string to file specified by `path`.
|
||||||
|
|
||||||
Signals will be appended to file specified by `path`.
|
Can output signals as human or machine-readable (edn, JSON) strings.
|
||||||
Depending on options, archives may be maintained:
|
|
||||||
|
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
|
||||||
|
|
||||||
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:
|
||||||
`:format-signal-fn`- (fn [signal]) => output, see `help:signal-formatters`.
|
`: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))
|
||||||
([{:keys
|
([{:keys
|
||||||
[format-signal-fn
|
[output-fn
|
||||||
path interval
|
path interval
|
||||||
max-file-size
|
max-file-size
|
||||||
max-num-parts
|
max-num-parts
|
||||||
|
|
@ -307,7 +309,7 @@
|
||||||
gzip-archives?]
|
gzip-archives?]
|
||||||
|
|
||||||
:or
|
:or
|
||||||
{format-signal-fn (utils/format-signal->str-fn)
|
{output-fn (utils/format-signal-fn)
|
||||||
path "logs/telemere.log" ; Main path, we'll ALWAYS write to this exact file
|
path "logs/telemere.log" ; Main path, we'll ALWAYS write to this exact file
|
||||||
interval :monthly
|
interval :monthly
|
||||||
max-file-size (* 1024 1024 4) ; 4MB
|
max-file-size (* 1024 1024 4) ; 4MB
|
||||||
|
|
@ -317,12 +319,12 @@
|
||||||
|
|
||||||
(let [main-path path
|
(let [main-path path
|
||||||
main-file (utils/as-file main-path)
|
main-file (utils/as-file main-path)
|
||||||
fw (utils/file-writer main-file true)
|
fw (utils/file-writer {:file main-file, :append? true})
|
||||||
|
|
||||||
>max-file-size?
|
>max-file-size?
|
||||||
(when max-file-size
|
(when max-file-size
|
||||||
(let [max-file-size (long max-file-size)
|
(let [max-file-size (long max-file-size)
|
||||||
rl (enc/rate-limiter-once-per 2500)]
|
rl (enc/rate-limiter-once-per 250)]
|
||||||
(fn [] (and (not (rl)) (> (.length main-file) max-file-size)))))
|
(fn [] (and (not (rl)) (> (.length main-file) max-file-size)))))
|
||||||
|
|
||||||
prev-timestamp_ (enc/latom nil) ; Initially nil
|
prev-timestamp_ (enc/latom nil) ; Initially nil
|
||||||
|
|
@ -359,11 +361,10 @@
|
||||||
lock (Object.)]
|
lock (Object.)]
|
||||||
|
|
||||||
(fn a-handler:file
|
(fn a-handler:file
|
||||||
([] (locking lock (fw))) ; Close writer
|
([ ] (locking lock (fw))) ; Stop => close writer
|
||||||
([signal]
|
([signal]
|
||||||
(when-let [output (format-signal-fn signal)]
|
(when-let [output (output-fn signal)]
|
||||||
(let [output-str (str output utils/newline)
|
(let [new-interval? (when interval (new-interval!?))
|
||||||
new-interval? (when interval (new-interval!?))
|
|
||||||
>max-file-size? (when max-file-size (>max-file-size?))
|
>max-file-size? (when max-file-size (>max-file-size?))
|
||||||
reset-stream? (or new-interval? >max-file-size?)]
|
reset-stream? (or new-interval? >max-file-size?)]
|
||||||
|
|
||||||
|
|
@ -386,13 +387,14 @@
|
||||||
max-num-parts gzip-archives? nil)))
|
max-num-parts gzip-archives? nil)))
|
||||||
|
|
||||||
(when reset-stream? (fw :writer/reset!))
|
(when reset-stream? (fw :writer/reset!))
|
||||||
(do (fw output-str))))))))))
|
(do (fw output))))))))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(manage-test-files! :create)
|
(manage-test-files! :create)
|
||||||
(.setLastModified (utils/as-file "test/logs/app6.log")
|
(.setLastModified (utils/as-file "test/logs/app6.log")
|
||||||
(enc/as-udt "1999-01-01T01:00:00.00Z"))
|
(enc/as-udt "1999-01-01T01:00:00.00Z"))
|
||||||
|
|
||||||
|
(let [f (utils/as-file "test/logs/app6.log")] (enc/qb 1e5 (.length f)))
|
||||||
(let [hfn
|
(let [hfn
|
||||||
(handler:file
|
(handler:file
|
||||||
{:path "test/logs/app6.log"
|
{:path "test/logs/app6.log"
|
||||||
810
main/src/taoensso/telemere/impl.cljc
Normal file
810
main/src/taoensso/telemere/impl.cljc
Normal 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)))))
|
||||||
411
main/src/taoensso/telemere/open_telemetry.clj
Normal file
411
main/src/taoensso/telemere/open_telemetry.clj
Normal 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"})))
|
||||||
114
main/src/taoensso/telemere/postal.clj
Normal file
114
main/src/taoensso/telemere/postal.clj
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
(ns taoensso.telemere.postal
|
||||||
|
"Telemere -> email handler using `postal`,
|
||||||
|
Ref. <https://github.com/drewr/postal>."
|
||||||
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.encore.signals :as sigs]
|
||||||
|
[taoensso.telemere.utils :as utils]
|
||||||
|
[postal.core :as postal]))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(require '[taoensso.telemere :as tel])
|
||||||
|
(remove-ns (symbol (str *ns*)))
|
||||||
|
(:api (enc/interns-overview)))
|
||||||
|
|
||||||
|
(def default-dispatch-opts
|
||||||
|
{:min-level :info
|
||||||
|
:limit
|
||||||
|
[[5 (enc/msecs :mins 1)]
|
||||||
|
[10 (enc/msecs :mins 15)]
|
||||||
|
[15 (enc/msecs :hours 1)]
|
||||||
|
[30 (enc/msecs :hours 6)]]})
|
||||||
|
|
||||||
|
(defn handler:postal
|
||||||
|
"Alpha, subject to change.
|
||||||
|
|
||||||
|
Needs `postal`, Ref. <https://github.com/drewr/postal>.
|
||||||
|
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Sends the signal as an email to specified recipient.
|
||||||
|
|
||||||
|
Useful for emailing important alerts to admins, etc.
|
||||||
|
|
||||||
|
Default handler dispatch options (override when calling `add-handler!`):
|
||||||
|
`:min-level` - `:info`
|
||||||
|
`:limit` -
|
||||||
|
[[5 (enc/msecs :mins 1)] ; Max 5 emails in 1 min
|
||||||
|
[10 (enc/msecs :mins 15)] ; Max 10 emails in 15 mins
|
||||||
|
[15 (enc/msecs :hours 1)] ; Max 15 emails in 1 hour
|
||||||
|
[30 (enc/msecs :hours 6)] ; Max 30 emails in 6 hours
|
||||||
|
]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:conn-opts` - Map of connection opts given to `postal/send-message`
|
||||||
|
Examples:
|
||||||
|
{:host \"mail.isp.net\", :user \"jsmith\", :pass \"a-secret\"},
|
||||||
|
{:host \"smtp.gmail.com\", :user \"jsmith@gmail.com\", :pass \"a-secret\" :port 587 :tls true},
|
||||||
|
{:host \"email-smtp.us-east-1.amazonaws.com\", :port 587, :tls true,
|
||||||
|
:user \"AKIAIDTP........\", :pass \"AikCFhx1P.......\"}
|
||||||
|
|
||||||
|
`:msg-opts` - Map of message opts given to `postal/send-message`
|
||||||
|
Examples:
|
||||||
|
{:from \"foo@example.com\", :to \"bar@example.com\"},
|
||||||
|
{:from \"Alice <foo@example.com\", :to \"Bob <bar@example.com>\"},
|
||||||
|
{:from \"no-reply@example.com\", :to [\"first-responders@example.com\",
|
||||||
|
\"devops@example.com\"],
|
||||||
|
:cc \"engineering@example.com\"
|
||||||
|
:X-MyHeader \"A custom header\"}
|
||||||
|
|
||||||
|
`: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,
|
||||||
|
see `format-signal-fn` or `pr-signal-fn`
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
- Ref. <https://github.com/drewr/postal> for more info on `postal` options.
|
||||||
|
- Sending emails can be slow, and can incur financial costs!
|
||||||
|
Use appropriate handler dispatch options for async handling and rate limiting, etc."
|
||||||
|
|
||||||
|
;; ([] (handler:postal nil))
|
||||||
|
([{:keys [conn-opts msg-opts, subject-fn subject-max-len body-fn]
|
||||||
|
:or
|
||||||
|
{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) (truss/ex-info! "Expected `:conn-opts` map" (truss/typed-val conn-opts)))
|
||||||
|
(when-not (map? msg-opts) (truss/ex-info! "Expected `:msg-opts` map" (truss/typed-val msg-opts)))
|
||||||
|
|
||||||
|
(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
|
||||||
|
([ ]) ; Stop => noop
|
||||||
|
([signal]
|
||||||
|
(enc/when-let [subject (subject-fn signal)
|
||||||
|
body (body-fn signal)]
|
||||||
|
(let [msg
|
||||||
|
(assoc msg-opts
|
||||||
|
:subject (str subject)
|
||||||
|
:body
|
||||||
|
(if (string? body)
|
||||||
|
[{:type "text/plain; charset=utf-8"
|
||||||
|
:content (str body)}]
|
||||||
|
body))
|
||||||
|
|
||||||
|
[result ex]
|
||||||
|
(try
|
||||||
|
[(postal/send-message conn-opts msg) nil]
|
||||||
|
(catch Exception ex [nil ex]))
|
||||||
|
|
||||||
|
success? (= (get result :code) 0)]
|
||||||
|
|
||||||
|
(when-not success?
|
||||||
|
(truss/ex-info! "Failed to send email" result ex))))))]
|
||||||
|
|
||||||
|
(with-meta handler-fn
|
||||||
|
{:dispatch-opts default-dispatch-opts}))))
|
||||||
83
main/src/taoensso/telemere/slack.clj
Normal file
83
main/src/taoensso/telemere/slack.clj
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
(ns taoensso.telemere.slack
|
||||||
|
"Telemere -> Slack handler using `clj-slack`,
|
||||||
|
Ref. <https://github.com/julienXX/clj-slack>"
|
||||||
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.telemere.utils :as utils]
|
||||||
|
[clj-slack.core :as slack]
|
||||||
|
[clj-slack.chat :as slack.chat]))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(require '[taoensso.telemere :as tel])
|
||||||
|
(remove-ns (symbol (str *ns*)))
|
||||||
|
(:api (enc/interns-overview)))
|
||||||
|
|
||||||
|
(def default-dispatch-opts
|
||||||
|
{:min-level :info
|
||||||
|
:limit
|
||||||
|
[[5 (enc/msecs :mins 1)]
|
||||||
|
[10 (enc/msecs :mins 15)]
|
||||||
|
[15 (enc/msecs :hours 1)]
|
||||||
|
[30 (enc/msecs :hours 6)]]})
|
||||||
|
|
||||||
|
(defn handler:slack
|
||||||
|
"Alpha, subject to change.
|
||||||
|
|
||||||
|
Needs `clj-slack`, Ref. <https://github.com/julienXX/clj-slack>.
|
||||||
|
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Writes the signal as a string to specified Slack channel.
|
||||||
|
|
||||||
|
Can output signals as human or machine-readable (edn, JSON) strings.
|
||||||
|
|
||||||
|
Default handler dispatch options (override when calling `add-handler!`):
|
||||||
|
`:min-level` - `:info`
|
||||||
|
`:limit` -
|
||||||
|
[[5 (enc/msecs :mins 1)] ; Max 5 posts in 1 min
|
||||||
|
[10 (enc/msecs :mins 15)] ; Max 10 posts in 15 mins
|
||||||
|
[15 (enc/msecs :hours 1)] ; Max 15 posts in 1 hour
|
||||||
|
[30 (enc/msecs :hours 6)] ; Max 30 posts in 6 hours
|
||||||
|
]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:output-fn` - (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
|
||||||
|
`:conn-opts` - Map of connection opts given to `clj-slack.chat/post-message`
|
||||||
|
Examples:
|
||||||
|
{:token \"MY-TOKEN\"}
|
||||||
|
{:token \"MY-TOKEN\", :api-url \"https://slack.com/api\"}
|
||||||
|
|
||||||
|
`:post-opts` - Map of post opts given to `clj-slack.chat/post-message`
|
||||||
|
Examples:
|
||||||
|
{:channel-id \"C12345678\", :username \"MY_BOT\"}
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
- See `clj-slack` docs for more info on its options."
|
||||||
|
|
||||||
|
;; ([] (handler:slack nil))
|
||||||
|
([{:keys [conn-opts post-opts output-fn]
|
||||||
|
:or
|
||||||
|
{conn-opts {:api-url "https://slack.com/api", :token nil}
|
||||||
|
post-opts {:channel-id nil, :username nil}
|
||||||
|
output-fn (utils/format-signal-fn)}}]
|
||||||
|
|
||||||
|
(let [{:keys [api-url token]
|
||||||
|
:or {api-url "https://slack.com/api"}} conn-opts
|
||||||
|
|
||||||
|
{:keys [channel-id]} post-opts
|
||||||
|
post-opts (dissoc post-opts :channel-id)
|
||||||
|
|
||||||
|
_ (when-not (string? token) (truss/ex-info! "Expected `:conn-opts/token` string" (truss/typed-val token)))
|
||||||
|
_ (when-not (string? channel-id) (truss/ex-info! "Expected `:post-opts/channel-id` string" (truss/typed-val channel-id)))
|
||||||
|
|
||||||
|
handler-fn
|
||||||
|
(fn a-handler:slack
|
||||||
|
([ ]) ; Stop => noop
|
||||||
|
([signal]
|
||||||
|
(when-let [output (output-fn signal)]
|
||||||
|
(slack.chat/post-message conn-opts channel-id
|
||||||
|
output post-opts))))]
|
||||||
|
|
||||||
|
(with-meta handler-fn
|
||||||
|
{:dispatch-opts default-dispatch-opts}))))
|
||||||
116
main/src/taoensso/telemere/sockets.clj
Normal file
116
main/src/taoensso/telemere/sockets.clj
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
(ns taoensso.telemere.sockets
|
||||||
|
"Telemere -> TCP/UDP socket handlers."
|
||||||
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.telemere.utils :as utils])
|
||||||
|
|
||||||
|
(:import
|
||||||
|
[java.net Socket InetAddress]
|
||||||
|
[java.net DatagramSocket DatagramPacket InetSocketAddress]
|
||||||
|
[java.io PrintWriter]))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(require '[taoensso.telemere :as tel])
|
||||||
|
(remove-ns (symbol (str *ns*)))
|
||||||
|
(:api (enc/interns-overview)))
|
||||||
|
|
||||||
|
(defn handler:tcp-socket
|
||||||
|
"Experimental, subject to change.
|
||||||
|
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Sends the signal as a string to specified TCP socket.
|
||||||
|
|
||||||
|
Can output signals as human or machine-readable (edn, JSON) strings.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:output-fn` --- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
|
||||||
|
`:socket-opts` - {:keys [host port ssl? connect-timeout-msecs]}
|
||||||
|
`:host` ------ Destination TCP socket hostname string
|
||||||
|
`:port` ------ Destination TCP socket port int
|
||||||
|
`:ssl?` ------ Use SSL/TLS (default false)
|
||||||
|
`:connect-timeout-msecs` - Connection timeout (default 3000 msecs)
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Failed writes will be retried only once.
|
||||||
|
- Writes lock on a single underlying socket, so IO won't benefit from adding
|
||||||
|
extra handler threads. Let me know if there's demand for socket pooling."
|
||||||
|
|
||||||
|
;; ([] (handler:tcp-socket nil))
|
||||||
|
([{:keys [socket-opts output-fn]
|
||||||
|
:or {output-fn (utils/format-signal-fn)}}]
|
||||||
|
|
||||||
|
(let [sw (utils/tcp-socket-writer socket-opts)]
|
||||||
|
(fn a-handler:tcp-socket
|
||||||
|
([ ] (sw)) ; Stop => close socket
|
||||||
|
([signal]
|
||||||
|
(when-let [output (output-fn signal)]
|
||||||
|
(sw output)))))))
|
||||||
|
|
||||||
|
(defn handler:udp-socket
|
||||||
|
"Highly experimental, subject to change!
|
||||||
|
Feedback very welcome!
|
||||||
|
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Sends the signal as a string to specified UDP socket.
|
||||||
|
|
||||||
|
Can output signals as human or machine-readable (edn, JSON) strings.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:output-fn` ---------- (fn [signal]) => string, see `format-signal-fn` or `pr-signal-fn`
|
||||||
|
`:socket-opts` -------- {:keys [host port max-packet-bytes]}
|
||||||
|
`:host` ------------- Destination UDP socket hostname string
|
||||||
|
`:port` ------------- Destination UDP socket port int
|
||||||
|
`:max-packet-bytes` - Max packet size (in bytes) before truncating output (default 512)
|
||||||
|
|
||||||
|
`:truncation-warning-fn`
|
||||||
|
Optional (fn [{:keys [max actual signal]}]) to call whenever output is truncated.
|
||||||
|
Should be appropriately rate-limited!
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Due to UDP limitations, truncates output to `max-packet-bytes`!
|
||||||
|
- Failed writes will be retried only once.
|
||||||
|
- Writes lock on a single underlying socket, so IO won't benefit from adding
|
||||||
|
extra handler threads. Let me know if there's demand for socket pooling.
|
||||||
|
- No DTLS (Datagram Transport Layer Security) support,
|
||||||
|
please let me know if there's demand."
|
||||||
|
|
||||||
|
;; ([] (handler:udp-socket nil))
|
||||||
|
([{:keys [socket-opts output-fn truncation-warning-fn]
|
||||||
|
:or
|
||||||
|
{socket-opts {:max-packet-bytes 512}
|
||||||
|
output-fn (utils/format-signal-fn)}}]
|
||||||
|
|
||||||
|
(let [{:keys [host port max-packet-bytes]
|
||||||
|
:or {max-packet-bytes 512}} socket-opts
|
||||||
|
|
||||||
|
max-packet-bytes (int max-packet-bytes)
|
||||||
|
|
||||||
|
socket (DatagramSocket.) ; No need to change socket once created
|
||||||
|
lock (Object.)]
|
||||||
|
|
||||||
|
(when-not (string? host) (truss/ex-info! "Expected `:host` string" (truss/typed-val host)))
|
||||||
|
(when-not (int? port) (truss/ex-info! "Expected `:port` int" (truss/typed-val port)))
|
||||||
|
|
||||||
|
(.connect socket (InetSocketAddress. (str host) (int port)))
|
||||||
|
|
||||||
|
(fn a-handler:udp-socket
|
||||||
|
([ ] (locking lock (.close socket))) ; Stop => close socket
|
||||||
|
([signal]
|
||||||
|
(when-let [output (output-fn signal)]
|
||||||
|
(let [ba (enc/str->utf8-ba (str output))
|
||||||
|
ba-len (alength ba)
|
||||||
|
packet (DatagramPacket. ba (min ba-len max-packet-bytes))]
|
||||||
|
|
||||||
|
(when (and truncation-warning-fn (> ba-len max-packet-bytes))
|
||||||
|
;; Fn should be appropriately rate-limited
|
||||||
|
(truncation-warning-fn {:max max-packet-bytes, :actual ba-len, :signal signal}))
|
||||||
|
|
||||||
|
(locking lock
|
||||||
|
(try
|
||||||
|
(.send (DatagramSocket.) packet)
|
||||||
|
(catch Exception _ ; Retry once
|
||||||
|
(Thread/sleep 250)
|
||||||
|
(.send (DatagramSocket.) packet)))))))))))
|
||||||
|
|
@ -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
|
||||||
|
|
@ -85,7 +84,7 @@
|
||||||
(let [monitor (Object.)]
|
(let [monitor (Object.)]
|
||||||
|
|
||||||
(defn ^:public streams->reset!
|
(defn ^:public streams->reset!
|
||||||
"Experimental, subject to change without notice!
|
"Experimental, subject to change.
|
||||||
Resets `System/out` and `System/err` to their original value (prior to any
|
Resets `System/out` and `System/err` to their original value (prior to any
|
||||||
`streams->telemere!` call)."
|
`streams->telemere!` call)."
|
||||||
[]
|
[]
|
||||||
|
|
@ -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)}})
|
||||||
|
|
||||||
|
|
@ -107,7 +106,7 @@
|
||||||
(boolean (or orig-out orig-err))))
|
(boolean (or orig-out orig-err))))
|
||||||
|
|
||||||
(defn ^:public streams->telemere!
|
(defn ^:public streams->telemere!
|
||||||
"Experimental, subject to change without notice!
|
"Experimental, subject to change.
|
||||||
|
|
||||||
When given `out`, sets JVM's `System/out` to flush to Telemere signals with those opts.
|
When given `out`, sets JVM's `System/out` to flush to Telemere signals with those opts.
|
||||||
When given `err`, sets JVM's `System/err` to flush to Telemere signals with those opts.
|
When given `err`, sets JVM's `System/err` to flush to Telemere signals with those opts.
|
||||||
|
|
@ -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)
|
||||||
212
main/src/taoensso/telemere/timbre.cljc
Normal file
212
main/src/taoensso/telemere/timbre.cljc
Normal 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})))})
|
||||||
89
main/src/taoensso/telemere/tools_logging.clj
Normal file
89
main/src/taoensso/telemere/tools_logging.clj
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
(ns taoensso.telemere.tools-logging
|
||||||
|
"tools.logging -> Telemere interop.
|
||||||
|
Telemere will attempt to load this ns automatically when possible.
|
||||||
|
|
||||||
|
Naming conventions:
|
||||||
|
`tools.logging` - For referring to the library.
|
||||||
|
`tools-logging` - For symbols, keywords, and this namespace.
|
||||||
|
`clojure.tools.logging` - For env config to match library's conventions."
|
||||||
|
|
||||||
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.telemere.impl :as impl]
|
||||||
|
[clojure.tools.logging :as ctl]))
|
||||||
|
|
||||||
|
(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))
|
||||||
|
|
||||||
|
(deftype TelemereLogger [logger-name]
|
||||||
|
;; `logger-name` is typically ns string
|
||||||
|
clojure.tools.logging.impl/Logger
|
||||||
|
(enabled? [_ level]
|
||||||
|
(when-debug (println [:tools-logging/enabled? level logger-name]))
|
||||||
|
(impl/signal-allowed?
|
||||||
|
{:ns logger-name
|
||||||
|
:kind :tools-logging
|
||||||
|
:level level}))
|
||||||
|
|
||||||
|
(write! [_ level throwable message]
|
||||||
|
(when-debug (println [:tools-logging/write! level logger-name]))
|
||||||
|
(impl/signal!
|
||||||
|
{:allow? true ; Pre-filtered by `enabled?` call
|
||||||
|
:ns logger-name
|
||||||
|
:kind :tools-logging
|
||||||
|
:level level
|
||||||
|
:error throwable
|
||||||
|
:msg message})
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(deftype TelemereLoggerFactory []
|
||||||
|
clojure.tools.logging.impl/LoggerFactory
|
||||||
|
(name [_ ] "taoensso.telemere")
|
||||||
|
(get-logger [_ logger-name] (TelemereLogger. (str logger-name))))
|
||||||
|
|
||||||
|
(defn tools-logging->telemere!
|
||||||
|
"Configures tools.logging to use Telemere as its logging
|
||||||
|
implementation (backend).
|
||||||
|
|
||||||
|
Called automatically if one of the following is \"true\":
|
||||||
|
1. JVM property: `clojure.tools.logging.to-telemere`
|
||||||
|
2. Env variable: `CLOJURE_TOOLS_LOGGING_TO_TELEMERE`
|
||||||
|
3. Classpath resource: `clojure.tools.logging.to-telemere`"
|
||||||
|
[]
|
||||||
|
(impl/signal!
|
||||||
|
{:kind :event
|
||||||
|
:level :debug ; < :info since runs on init
|
||||||
|
:id :taoensso.telemere/tools-logging->telemere!
|
||||||
|
:msg "Enabling interop: tools.logging -> Telemere"})
|
||||||
|
|
||||||
|
(alter-var-root #'clojure.tools.logging/*logger-factory*
|
||||||
|
(fn [_] (TelemereLoggerFactory.))))
|
||||||
|
|
||||||
|
(defn tools-logging->telemere?
|
||||||
|
"Returns true iff tools.logging is configured to use Telemere
|
||||||
|
as its logging implementation (backend)."
|
||||||
|
[]
|
||||||
|
(when-let [lf clojure.tools.logging/*logger-factory*]
|
||||||
|
(instance? TelemereLoggerFactory lf)))
|
||||||
|
|
||||||
|
;;;;
|
||||||
|
|
||||||
|
(defn check-interop
|
||||||
|
"Returns interop debug info map."
|
||||||
|
[]
|
||||||
|
(let [sending? (tools-logging->telemere?)
|
||||||
|
receiving?
|
||||||
|
(and sending?
|
||||||
|
(impl/test-interop! "tools.logging -> Telemere"
|
||||||
|
#(clojure.tools.logging/info %)))]
|
||||||
|
|
||||||
|
{:present? true
|
||||||
|
:enabled-by-env? impl/enabled:tools-logging?
|
||||||
|
:sending->telemere? sending?
|
||||||
|
:telemere-receiving? receiving?}))
|
||||||
|
|
||||||
|
(impl/add-interop-check! :tools-logging check-interop)
|
||||||
|
|
||||||
|
(impl/on-init
|
||||||
|
(when impl/enabled:tools-logging?
|
||||||
|
(tools-logging->telemere!)))
|
||||||
822
main/src/taoensso/telemere/utils.cljc
Normal file
822
main/src/taoensso/telemere/utils.cljc
Normal file
|
|
@ -0,0 +1,822 @@
|
||||||
|
(ns taoensso.telemere.utils
|
||||||
|
"Misc utils useful for Telemere handlers, transforms, etc."
|
||||||
|
(:refer-clojure :exclude [newline])
|
||||||
|
(:require
|
||||||
|
[clojure.string :as str]
|
||||||
|
#?(:clj [clojure.java.io :as jio])
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.encore.signals :as sigs]
|
||||||
|
[taoensso.telemere.impl :as impl]))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(require '[taoensso.telemere :as tel])
|
||||||
|
(remove-ns (symbol (str *ns*)))
|
||||||
|
(:api (enc/interns-overview)))
|
||||||
|
|
||||||
|
;;;;
|
||||||
|
|
||||||
|
(enc/defaliases #_sigs/upper-qn sigs/format-level sigs/format-id)
|
||||||
|
|
||||||
|
;;;; Unique IDs (UIDs)
|
||||||
|
|
||||||
|
(defn nano-uid-fn
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn nano-uid [root?]) that returns a random nano-style uid string like:
|
||||||
|
\"r76-B8LoIPs5lBG1_Uhdy\" - 126 bit (21 char) root uid
|
||||||
|
\"tMEYoZH0K-\" - 60 bit (10 char) non-root (child) uid"
|
||||||
|
{:tag #?(:clj 'String :cljs 'string)}
|
||||||
|
([] (nano-uid-fn nil))
|
||||||
|
([{:keys [secure? root-len child-len]
|
||||||
|
:or
|
||||||
|
{root-len 21
|
||||||
|
child-len 10}}]
|
||||||
|
|
||||||
|
(let [root-len (long root-len)
|
||||||
|
child-len (long child-len)]
|
||||||
|
|
||||||
|
#?(:cljs (fn nano-uid [root?] (if root? (enc/nanoid secure? root-len) (enc/nanoid secure? child-len)))
|
||||||
|
:clj
|
||||||
|
(if secure?
|
||||||
|
(fn nano-uid-secure [root?]
|
||||||
|
(let [srng (.get com.taoensso.encore.Ids/SRNG_STRONG)]
|
||||||
|
(if root?
|
||||||
|
(com.taoensso.encore.Ids/genNanoId srng root-len)
|
||||||
|
(com.taoensso.encore.Ids/genNanoId srng child-len))))
|
||||||
|
|
||||||
|
(fn nano-uid-insecure [root?]
|
||||||
|
(if root?
|
||||||
|
(com.taoensso.encore.Ids/genNanoId root-len)
|
||||||
|
(com.taoensso.encore.Ids/genNanoId child-len))))))))
|
||||||
|
|
||||||
|
(comment ((nano-uid-fn) true))
|
||||||
|
|
||||||
|
(defn hex-uid-fn
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn hex-uid [root?]) that returns a random hex-style uid string like:
|
||||||
|
\"05039666eb9dc3206475f44ab9f3d843\" - 128 bit (32 char) root uid
|
||||||
|
\"721fcef639a51513\" - 64 bit (16 char) non-root (child) uid"
|
||||||
|
{:tag #?(:clj 'String :cljs 'string)}
|
||||||
|
([] (hex-uid-fn nil))
|
||||||
|
([{:keys [secure? root-len child-len]
|
||||||
|
:or
|
||||||
|
{root-len 32
|
||||||
|
child-len 16}}]
|
||||||
|
|
||||||
|
(let [root-len (long root-len)
|
||||||
|
child-len (long child-len)]
|
||||||
|
|
||||||
|
#?(:cljs
|
||||||
|
(let [rand-bytes-fn
|
||||||
|
(if secure?
|
||||||
|
(partial enc/rand-bytes true)
|
||||||
|
(partial enc/rand-bytes false))
|
||||||
|
|
||||||
|
hex-uid-root (enc/rand-id-fn {:chars :hex-lowercase, :len root-len, :rand-bytes-fn rand-bytes-fn})
|
||||||
|
hex-uid-child (enc/rand-id-fn {:chars :hex-lowercase, :len child-len, :rand-bytes-fn rand-bytes-fn})]
|
||||||
|
|
||||||
|
(fn hex-uid [root?] (if root? (hex-uid-root) (hex-uid-child))))
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(if secure?
|
||||||
|
(fn hex-uid-secure [root?]
|
||||||
|
(let [srng (.get com.taoensso.encore.Ids/SRNG_STRONG)]
|
||||||
|
(if root?
|
||||||
|
(com.taoensso.encore.Ids/genHexId srng root-len)
|
||||||
|
(com.taoensso.encore.Ids/genHexId srng child-len))))
|
||||||
|
|
||||||
|
(fn hex-uid-insecure [root?]
|
||||||
|
(if root?
|
||||||
|
(com.taoensso.encore.Ids/genHexId root-len)
|
||||||
|
(com.taoensso.encore.Ids/genHexId child-len))))))))
|
||||||
|
|
||||||
|
(comment ((hex-uid-fn) true))
|
||||||
|
(comment
|
||||||
|
;; [168.74 180.83 65.28 47.3]
|
||||||
|
(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))))
|
||||||
|
|
||||||
|
(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
|
||||||
|
|
||||||
|
(enc/defaliases
|
||||||
|
enc/newline enc/pr-edn #?(:clj enc/uuid) enc/uuid-str
|
||||||
|
#?@(:cljs [enc/pr-json])
|
||||||
|
#?@(:clj [enc/thread-info enc/thread-id enc/thread-name
|
||||||
|
enc/host-info enc/host-ip enc/hostname]))
|
||||||
|
|
||||||
|
#?(:cljs
|
||||||
|
(defn js-console-logger
|
||||||
|
"Returns JavaScript console logger to match given signal level:
|
||||||
|
`:debug` -> `js/console.debug`,
|
||||||
|
`:error` -> `js/console.error`, etc.
|
||||||
|
|
||||||
|
Defaults to `js.console.log` for unmatched signal levels.
|
||||||
|
NB: assumes that `js/console` exists, handler constructors should check first!"
|
||||||
|
[level]
|
||||||
|
(case level
|
||||||
|
:trace js/console.debug
|
||||||
|
:debug js/console.debug
|
||||||
|
:info js/console.info
|
||||||
|
:warn js/console.warn
|
||||||
|
:error js/console.error
|
||||||
|
:fatal js/console.error
|
||||||
|
:report js/console.info
|
||||||
|
(do js/console.log))))
|
||||||
|
|
||||||
|
(comment (js-console-logger))
|
||||||
|
|
||||||
|
(defn error-signal?
|
||||||
|
"Returns true iff given signal has an `:error` value, or a `:kind` or `:level`
|
||||||
|
that indicates that it's an error."
|
||||||
|
#?(:cljs {:tag 'boolean})
|
||||||
|
[signal]
|
||||||
|
(and signal
|
||||||
|
(boolean
|
||||||
|
(or
|
||||||
|
(get signal :error)
|
||||||
|
(enc/identical-kw? (get signal :kind) :error)
|
||||||
|
(case (get signal :level) (:error :fatal) true false)
|
||||||
|
(get signal :error?) ; Provided kv
|
||||||
|
))))
|
||||||
|
|
||||||
|
(comment (error-signal? {:level :fatal}))
|
||||||
|
|
||||||
|
(defn ^:no-doc remove-signal-kvs
|
||||||
|
"Private, don't use.
|
||||||
|
Returns given signal without app-level kvs or `:kvs` key."
|
||||||
|
[signal]
|
||||||
|
(if-let [kvs (get signal :kvs)]
|
||||||
|
(reduce-kv (fn [m k _v] (dissoc m k)) (dissoc signal :kvs) kvs)
|
||||||
|
signal))
|
||||||
|
|
||||||
|
(defn ^:no-doc remove-signal-nils
|
||||||
|
"Private, don't use.
|
||||||
|
Returns given signal with nil-valued keys removed."
|
||||||
|
[signal]
|
||||||
|
(if (enc/editable? signal)
|
||||||
|
(persistent! (reduce-kv (fn [m k v] (if (nil? v) (dissoc! m k) m)) (transient signal) signal))
|
||||||
|
(persistent! (reduce-kv (fn [m k v] (if (nil? v) m (assoc! m k v))) (transient {}) signal))))
|
||||||
|
|
||||||
|
(defn ^:no-doc force-signal-msg
|
||||||
|
"Private, don't use.
|
||||||
|
Returns given signal with possible `:msg_` value forced (realized when a delay)."
|
||||||
|
[signal]
|
||||||
|
(if-let [msg_ (get signal :msg_)]
|
||||||
|
(assoc signal :msg_ (force msg_))
|
||||||
|
(do signal)))
|
||||||
|
|
||||||
|
(defn ^:no-doc expand-signal-error
|
||||||
|
"Private, don't use.
|
||||||
|
Returns given signal with possible `:error` replaced by
|
||||||
|
[{:keys [type msg data]} ...] cause chain."
|
||||||
|
[signal]
|
||||||
|
(enc/if-let [error (get signal :error)
|
||||||
|
chain (truss/ex-chain :as-map error)]
|
||||||
|
(assoc signal :error chain)
|
||||||
|
(do signal)))
|
||||||
|
|
||||||
|
;;;; Files
|
||||||
|
|
||||||
|
#?(:clj (defn ^:no-doc as-file ^java.io.File [file] (jio/as-file file)))
|
||||||
|
#?(:clj
|
||||||
|
(defn ^:no-doc writeable-file!
|
||||||
|
"Private, don't use.
|
||||||
|
Returns writable `java.io.File`, or throws."
|
||||||
|
^java.io.File [file]
|
||||||
|
(let [file (as-file file)]
|
||||||
|
(when-not (.exists file)
|
||||||
|
(truss/catching
|
||||||
|
(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)
|
||||||
|
file
|
||||||
|
(truss/ex-info! "Unable to prepare writable `java.io.File`"
|
||||||
|
{:path (.getAbsolutePath file)})))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(let [f (writeable-file! "__test-file.txt")]
|
||||||
|
(enc/qb 1e4 ; [10.27 37.69]
|
||||||
|
(.exists f)
|
||||||
|
(.canWrite f))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn ^:no-doc writeable-file-stream!
|
||||||
|
"Private, don't use.
|
||||||
|
Returns new writeable `java.io.FileOutputStream` or throws."
|
||||||
|
^java.io.FileOutputStream [file append?]
|
||||||
|
(java.io.FileOutputStream. (writeable-file! file) (boolean append?))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn file-writer
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Opens the specified file and returns a stateful fn of 2 arities:
|
||||||
|
[content] => Writes given content to file, or noops if closed.
|
||||||
|
[] => Closes the writer.
|
||||||
|
|
||||||
|
Useful for basic handlers that write to a file, etc.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Automatically creates file and parent dirs as necessary.
|
||||||
|
- Writer should be manually closed after use (with zero-arity call).
|
||||||
|
- Flushes after every write.
|
||||||
|
- Thread safe, locks on single file stream."
|
||||||
|
|
||||||
|
[{:keys [file append?]
|
||||||
|
:or {append? true}}]
|
||||||
|
|
||||||
|
(when-not file (truss/ex-info! "Expected `:file` value" (truss/typed-val file)))
|
||||||
|
|
||||||
|
(let [file (as-file file)
|
||||||
|
stream_ (volatile! (writeable-file-stream! file append?))
|
||||||
|
open?_ (enc/latom true)
|
||||||
|
|
||||||
|
close!
|
||||||
|
(fn []
|
||||||
|
(when (compare-and-set! open?_ true false)
|
||||||
|
(when-let [^java.io.FileOutputStream stream (.deref stream_)]
|
||||||
|
(.close stream)
|
||||||
|
(vreset! stream_ nil)
|
||||||
|
true)))
|
||||||
|
|
||||||
|
reset!
|
||||||
|
(fn []
|
||||||
|
(close!)
|
||||||
|
(vreset! stream_ (writeable-file-stream! file append?))
|
||||||
|
(reset! open?_ true)
|
||||||
|
true)
|
||||||
|
|
||||||
|
write-ba!
|
||||||
|
(fn [^bytes ba-content]
|
||||||
|
(when-let [^java.io.FileOutputStream stream (.deref stream_)]
|
||||||
|
(.write stream ba-content)
|
||||||
|
(.flush stream)
|
||||||
|
true))
|
||||||
|
|
||||||
|
check-file!
|
||||||
|
(let [rl (enc/rate-limiter-once-per 100)]
|
||||||
|
(fn []
|
||||||
|
(or (rl) #_(.exists file) (.canWrite file)
|
||||||
|
(throw (java.io.IOException. "File doesn't exist or isn't writeable")))))
|
||||||
|
|
||||||
|
lock (Object.)]
|
||||||
|
|
||||||
|
(fn a-file-writer
|
||||||
|
([] (when (open?_) (locking lock (close!))))
|
||||||
|
([content-or-action]
|
||||||
|
(case content-or-action ; Undocumented, for dev/testing
|
||||||
|
:writer/open? (open?_)
|
||||||
|
:writer/reset! (locking lock (reset!))
|
||||||
|
:writer/state {:file file, :stream (.deref stream_)}
|
||||||
|
(when (open?_)
|
||||||
|
(let [content content-or-action
|
||||||
|
ba (enc/str->utf8-ba (str content))]
|
||||||
|
(locking lock
|
||||||
|
(try
|
||||||
|
(check-file!)
|
||||||
|
(write-ba! ba)
|
||||||
|
(catch java.io.IOException _ ; Retry once
|
||||||
|
(reset!)
|
||||||
|
(write-ba! ba))))))))))))
|
||||||
|
|
||||||
|
(comment (def fw1 (file-writer {:file "test.txt"})) (fw1 "x") (fw1))
|
||||||
|
|
||||||
|
;;;; Sockets
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn- default-socket-fn
|
||||||
|
"Returns conected `java.net.Socket`, or throws."
|
||||||
|
^java.net.Socket [host port connect-timeout-msecs]
|
||||||
|
(let [addr (java.net.InetSocketAddress. ^String host (int port))
|
||||||
|
socket (java.net.Socket.)]
|
||||||
|
|
||||||
|
(if connect-timeout-msecs
|
||||||
|
(.connect socket addr (int connect-timeout-msecs))
|
||||||
|
(.connect socket addr))
|
||||||
|
|
||||||
|
socket)))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(let [factory_ (delay (javax.net.ssl.SSLSocketFactory/getDefault))]
|
||||||
|
(defn- default-ssl-socket-fn
|
||||||
|
"Returns connected SSL `java.net.Socket`, or throws."
|
||||||
|
^java.net.Socket [^java.net.Socket socket ^String host port]
|
||||||
|
(.createSocket ^javax.net.ssl.SSLSocketFactory @factory_
|
||||||
|
socket host (int port) true))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn tcp-socket-writer
|
||||||
|
"Experimental, subject to change.
|
||||||
|
Connects to specified TCP socket and returns a stateful fn of 2 arities:
|
||||||
|
[content] => Writes given content to socket, or noops if closed.
|
||||||
|
[] => Closes the writer.
|
||||||
|
|
||||||
|
Useful for basic handlers that write to a TCP socket, etc.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:ssl?` ------------------ Use SSL/TLS?
|
||||||
|
`:connect-timeout-msecs` - Connection timeout (default 3000 msecs)
|
||||||
|
`:socket-fn` ------------- (fn [host port timeout]) => `java.net.Socket`
|
||||||
|
`:ssl-socket-fn` --------- (fn [socket host port]) => `java.net.Socket`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Writer should be manually closed after use (with zero-arity call).
|
||||||
|
- Flushes after every write.
|
||||||
|
- Will retry failed writes once, then drop.
|
||||||
|
- Thread safe, locks on single socket stream.
|
||||||
|
- Advanced users may want a custom implementation using a connection
|
||||||
|
pool and/or more sophisticated retry semantics, etc."
|
||||||
|
|
||||||
|
[{:keys
|
||||||
|
[host port, ssl? connect-timeout-msecs,
|
||||||
|
socket-fn ssl-socket-fn] :as opts
|
||||||
|
|
||||||
|
:or
|
||||||
|
{connect-timeout-msecs 3000
|
||||||
|
socket-fn default-socket-fn
|
||||||
|
ssl-socket-fn default-ssl-socket-fn}}]
|
||||||
|
|
||||||
|
(when-not (string? host) (truss/ex-info! "Expected `:host` string" (truss/typed-val host)))
|
||||||
|
(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
|
||||||
|
(fn []
|
||||||
|
(try
|
||||||
|
(let [^java.net.Socket socket
|
||||||
|
(let [socket (socket-fn host port connect-timeout-msecs)]
|
||||||
|
(if ssl?
|
||||||
|
(ssl-socket-fn socket host port)
|
||||||
|
(do socket)))]
|
||||||
|
|
||||||
|
[socket (.getOutputStream socket)])
|
||||||
|
|
||||||
|
(catch Exception ex
|
||||||
|
(truss/ex-info! "Failed to create connection" opts ex))))
|
||||||
|
|
||||||
|
conn_ (volatile! (new-conn!))
|
||||||
|
open?_ (enc/latom true)
|
||||||
|
|
||||||
|
close!
|
||||||
|
(fn []
|
||||||
|
(when (compare-and-set! open?_ true false)
|
||||||
|
(when-let [[^java.net.Socket socket] (.deref conn_)]
|
||||||
|
(.close socket)
|
||||||
|
(vreset! conn_ nil)
|
||||||
|
true)))
|
||||||
|
|
||||||
|
reset!
|
||||||
|
(fn []
|
||||||
|
(close!)
|
||||||
|
(vreset! conn_ (new-conn!))
|
||||||
|
(reset! open?_ true)
|
||||||
|
true)
|
||||||
|
|
||||||
|
write-ba!
|
||||||
|
(fn [^bytes ba-content]
|
||||||
|
(when-let [[_ ^java.io.OutputStream output] (.deref conn_)]
|
||||||
|
(.write output ba-content)
|
||||||
|
(.flush output)
|
||||||
|
true))
|
||||||
|
|
||||||
|
conn-okay!
|
||||||
|
(let [rl (enc/rate-limiter-once-per 100)]
|
||||||
|
(fn []
|
||||||
|
(or
|
||||||
|
(rl)
|
||||||
|
(when-let [[^java.net.Socket socket] (.deref conn_)]
|
||||||
|
(and
|
||||||
|
(not (.isClosed socket))
|
||||||
|
(do (.isConnected socket))))
|
||||||
|
(throw (java.io.IOException. "Bad connection")))))
|
||||||
|
|
||||||
|
lock (Object.)]
|
||||||
|
|
||||||
|
(fn a-tcp-socket-writer
|
||||||
|
([] (when (open?_) (locking lock (close!))))
|
||||||
|
([content-or-action]
|
||||||
|
(case content-or-action ; Undocumented, for dev/testing
|
||||||
|
:writer/open? (open?_)
|
||||||
|
:writer/reset! (locking lock (reset!))
|
||||||
|
:writer/state {:conn (.deref conn_)}
|
||||||
|
(when (open?_)
|
||||||
|
(let [content content-or-action
|
||||||
|
ba (enc/str->utf8-ba (str content))]
|
||||||
|
(locking lock
|
||||||
|
(try
|
||||||
|
(conn-okay!)
|
||||||
|
(write-ba! ba)
|
||||||
|
(catch Exception _ ; Retry once
|
||||||
|
(reset!)
|
||||||
|
(write-ba! ba))))))))))))
|
||||||
|
|
||||||
|
;;;; Formatters
|
||||||
|
|
||||||
|
(defn format-nsecs-fn
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn format [nanosecs]) that:
|
||||||
|
- Takes a long nanoseconds (e.g. runtime).
|
||||||
|
- Returns a human-readable string like:
|
||||||
|
\"1.00m\", \"4.20s\", \"340ms\", \"822μs\", etc."
|
||||||
|
([] (format-nsecs-fn nil))
|
||||||
|
([{:as _opts}] (fn format-nsecs [nanosecs] (enc/format-nsecs nanosecs))))
|
||||||
|
|
||||||
|
(comment ((format-nsecs-fn) 4747463567))
|
||||||
|
|
||||||
|
(enc/defalias enc/format-inst-fn)
|
||||||
|
|
||||||
|
(comment ((format-inst-fn) (enc/now-inst)))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn- format-clj-stacktrace
|
||||||
|
[trace]
|
||||||
|
(let [sb (enc/str-builder)
|
||||||
|
s+nl (enc/sb-appender sb enc/newline)]
|
||||||
|
(doseq [st-el (force trace)]
|
||||||
|
(let [{:keys [class method file line]} st-el]
|
||||||
|
(s+nl " " class "/" method " at " file ":" line)))
|
||||||
|
(str sb))))
|
||||||
|
|
||||||
|
(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
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn format [error]) that:
|
||||||
|
- Takes a platform error (`Throwable` or `js/Error`).
|
||||||
|
- Returns a human-readable error string."
|
||||||
|
([] (format-error-fn nil))
|
||||||
|
([{:as _opts}]
|
||||||
|
(let [nl enc/newline
|
||||||
|
nls enc/newlines]
|
||||||
|
|
||||||
|
(fn format-error [error]
|
||||||
|
(when-let [em (truss/ex-map error)]
|
||||||
|
(let [sb (enc/str-builder)
|
||||||
|
s+ (partial enc/sb-append sb)
|
||||||
|
{:keys [chain trace]} em]
|
||||||
|
|
||||||
|
(let [s+cause (enc/sb-appender sb (str nls "Caused: "))]
|
||||||
|
(s+ "Root: ")
|
||||||
|
(doseq [{:keys [type msg data]} (rseq chain)]
|
||||||
|
(s+cause type " - " msg)
|
||||||
|
(when data
|
||||||
|
(s+ nl "data: " (enc/pr-edn* data)))))
|
||||||
|
|
||||||
|
(when trace
|
||||||
|
(s+ nl nl "Root stack trace:" nl)
|
||||||
|
#?(:cljs (s+ trace)
|
||||||
|
:clj (s+ (format-clj-stacktrace trace))))
|
||||||
|
|
||||||
|
(str sb)))))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(do (throw (truss/ex-info "Ex2" {:k2 "v2"} (truss/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) (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"}))))))
|
||||||
|
|
||||||
|
;;;;
|
||||||
|
|
||||||
|
(defn signal-preamble-fn
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn preamble [signal]) that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Returns a signal preamble ?string like:
|
||||||
|
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere[2,21] ::ev-id msg\"
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`: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))
|
||||||
|
([{:keys [format-inst-fn format-id-fn format-msg-fn]
|
||||||
|
:or {format-inst-fn (format-inst-fn)
|
||||||
|
format-id-fn format-id
|
||||||
|
format-msg-fn identity}}]
|
||||||
|
|
||||||
|
(fn signal-preamble [signal]
|
||||||
|
(let [{:keys [inst level kind ns id msg_]} signal
|
||||||
|
sb (enc/str-builder)
|
||||||
|
s+spc (enc/sb-appender sb " ")]
|
||||||
|
|
||||||
|
(when inst (when-let [ff format-inst-fn] (s+spc (ff inst))))
|
||||||
|
(when level (s+spc (format-level level)))
|
||||||
|
(when kind (s+spc (sigs/upper-qn kind)))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(when-let [hostname (enc/get-in* signal [:host :name])]
|
||||||
|
(s+spc hostname)))
|
||||||
|
|
||||||
|
(when ns (s+spc (sigs/format-callsite ns (get signal :coords))))
|
||||||
|
(when id (when-let [ff format-id-fn] (s+spc (ff ns id))))
|
||||||
|
(enc/when-let [ff format-msg-fn
|
||||||
|
msg (force msg_)]
|
||||||
|
(s+spc (ff msg)))
|
||||||
|
|
||||||
|
(when-not (zero? (enc/sb-length sb))
|
||||||
|
(str sb))))))
|
||||||
|
|
||||||
|
(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
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn content [signal]) that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Returns a human-readable signal content ?string (incl. data, ctx, etc.).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:raw-error?` ------ Retain unformatted error? (default false)
|
||||||
|
`:incl-keys` ------- Subset of signal keys to retain from those
|
||||||
|
otherwise excluded by default: #{:kvs :host :thread}
|
||||||
|
`:format-nsecs-fn` - (fn [nanosecs]) => string.
|
||||||
|
`:format-error-fn` - (fn [error]) => string."
|
||||||
|
|
||||||
|
([] (signal-content-fn nil))
|
||||||
|
([{:keys [raw-error? incl-keys, format-nsecs-fn format-error-fn]
|
||||||
|
:or
|
||||||
|
{format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
|
||||||
|
format-error-fn (format-error-fn) ; (fn [error])
|
||||||
|
}}]
|
||||||
|
|
||||||
|
(let [nl newline
|
||||||
|
err-start (str nl "<<< error <<<" nl)
|
||||||
|
err-stop (str nl ">>> error >>>")
|
||||||
|
incl-kvs? (contains? incl-keys :kvs)
|
||||||
|
incl-host? (contains? incl-keys :host)
|
||||||
|
incl-thread? (contains? incl-keys :thread)]
|
||||||
|
|
||||||
|
(fn signal-content
|
||||||
|
([signal]
|
||||||
|
(let [sb (enc/str-builder)
|
||||||
|
s++ (enc/sb-appender sb nl)]
|
||||||
|
(signal-content signal s++ enc/pr-edn*)
|
||||||
|
(when-not (zero? (enc/sb-length sb))
|
||||||
|
(str sb))))
|
||||||
|
|
||||||
|
;; Undocumented, advanced arity
|
||||||
|
([signal append-fn val-fn]
|
||||||
|
(let [af append-fn
|
||||||
|
vf val-fn]
|
||||||
|
|
||||||
|
(let [{:keys [ns uid parent root data kvs ctx #?@(:clj [host thread]) sample]} signal]
|
||||||
|
(when sample (af " sample: " (vf sample)))
|
||||||
|
(when uid (af " uid: " (vf uid)))
|
||||||
|
(when (and parent (not= parent root)) (af " parent: " (vf (format-parent ns parent)))) ; {:keys [id uid]}
|
||||||
|
(when root (af " root: " (vf (format-parent ns root)))) ; {:keys [id uid]}
|
||||||
|
|
||||||
|
#?(:clj (when (enc/and? host incl-host?) (af " host: " (vf host)))) ; {:keys [ name ip]}
|
||||||
|
#?(:clj (when (enc/and? thread incl-thread?) (af " thread: " (vf thread)))) ; {:keys [group name id]}
|
||||||
|
(when (enc/not-empty-coll data) (af " data: " (vf data)))
|
||||||
|
(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]
|
||||||
|
(when run-form
|
||||||
|
(let [{:keys [run-val run-nsecs]} signal
|
||||||
|
run-time (when run-nsecs (when-let [ff format-nsecs-fn] (ff run-nsecs)))
|
||||||
|
run-info
|
||||||
|
(if error
|
||||||
|
{:form run-form
|
||||||
|
:time run-time
|
||||||
|
:nsecs run-nsecs}
|
||||||
|
|
||||||
|
{:form run-form
|
||||||
|
:time run-time
|
||||||
|
:nsecs run-nsecs
|
||||||
|
:val run-val
|
||||||
|
#?@(:clj [:val-type (enc/class-sym run-val)])})]
|
||||||
|
(af " run: " (vf run-info))))
|
||||||
|
|
||||||
|
(when error
|
||||||
|
(if raw-error?
|
||||||
|
(af " error: " error)
|
||||||
|
(when-let [ff format-error-fn]
|
||||||
|
(af err-start (ff error) err-stop)))))))))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
((signal-content-fn) (tel/with-signal (tel/event! ::ev-id)))
|
||||||
|
((signal-content-fn) (tel/with-signal (tel/event! ::ev-id {:data {:k1 "v1"}}))))
|
||||||
|
|
||||||
|
(defn clean-signal-fn
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn clean [signal]) that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- 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:
|
||||||
|
`: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)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
;; To print as edn:
|
||||||
|
(pr-signal-fn {:pr-fn :edn})
|
||||||
|
|
||||||
|
;; To print as JSON:
|
||||||
|
;; 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)})
|
||||||
|
|
||||||
|
[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))
|
||||||
|
([{:keys [pr-fn clean-fn incl-newline?] :as opts
|
||||||
|
:or
|
||||||
|
{pr-fn :edn
|
||||||
|
clean-fn (clean-signal-fn)
|
||||||
|
incl-newline? true}}]
|
||||||
|
|
||||||
|
(let [nl newline
|
||||||
|
pr-fn
|
||||||
|
(or
|
||||||
|
(case pr-fn
|
||||||
|
:edn pr-edn
|
||||||
|
:json
|
||||||
|
#?(:cljs pr-json
|
||||||
|
:clj
|
||||||
|
(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)."
|
||||||
|
{}))
|
||||||
|
|
||||||
|
(if (fn? pr-fn)
|
||||||
|
(do pr-fn)
|
||||||
|
(truss/unexpected-arg! pr-fn
|
||||||
|
{:param 'pr-fn
|
||||||
|
:context `pr-signal-fn
|
||||||
|
:expected
|
||||||
|
#?(:clj '#{:edn unary-fn}
|
||||||
|
:cljs '#{:edn :json unary-fn})}))))]
|
||||||
|
|
||||||
|
(fn pr-signal [signal]
|
||||||
|
(when (map? signal)
|
||||||
|
(if incl-newline?
|
||||||
|
(str (pr-fn (clean-fn signal)) nl)
|
||||||
|
(do (pr-fn (clean-fn signal)))))))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
((pr-signal-fn {:pr-fn :edn})
|
||||||
|
(tel/with-signal (tel/event! ::ev-id {:kvs {:k1 "v1"}}))))
|
||||||
|
|
||||||
|
(defn format-signal-fn
|
||||||
|
"Alpha, subject to change.
|
||||||
|
Returns a (fn format [signal]) that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Returns a human-readable signal string.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`:incl-newline?` - Include terminating system newline? (default true)
|
||||||
|
`:preamble-fn` --- (fn [signal]) => signal preamble string, see [1]
|
||||||
|
`: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.)."
|
||||||
|
|
||||||
|
([] (format-signal-fn nil))
|
||||||
|
([{:keys [incl-newline? preamble-fn content-fn]
|
||||||
|
:or
|
||||||
|
{incl-newline? true
|
||||||
|
preamble-fn (signal-preamble-fn)
|
||||||
|
content-fn (signal-content-fn)}}]
|
||||||
|
|
||||||
|
(let [nl newline]
|
||||||
|
(fn format-signal [signal]
|
||||||
|
(let [preamble (when preamble-fn (preamble-fn signal))
|
||||||
|
content (when content-fn (content-fn signal))]
|
||||||
|
|
||||||
|
(if (and preamble content)
|
||||||
|
(str preamble nl content (when incl-newline? nl))
|
||||||
|
(str preamble content (when incl-newline? nl))))))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(tel/with-ctx {:c :C}
|
||||||
|
(println
|
||||||
|
((format-signal-fn)
|
||||||
|
(tel/with-signal
|
||||||
|
(tel/event! ::ev-id
|
||||||
|
{:my-k1 #{:a :b :c}
|
||||||
|
:msg "hi"
|
||||||
|
:data {:a :A}
|
||||||
|
;; :error (truss/ex-info "Ex2" {:k2 "v2"} (truss/ex-info "Ex1" {:k1 "v1"}))
|
||||||
|
:run (/ 1 0)}))))))
|
||||||
1085
main/test/taoensso/telemere_tests.cljc
Normal file
1085
main/test/taoensso/telemere_tests.cljc
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,38 +0,0 @@
|
||||||
Unconditionally executes given 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 level: `:error`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
(catch->error! (/ 1 0)) ; %> {:kind :error, :level :error, :error <caught> ...}
|
|
||||||
(catch->error! ::my-id (/ 1 0)) ; %> {... :id ::my-id ...}
|
|
||||||
(catch->error!
|
|
||||||
{:let [x "x"] ; Available to `:data` and `:msg`
|
|
||||||
:data {:x x}
|
|
||||||
:msg ["My msg:" x my-error]
|
|
||||||
:catch-val "Return value when 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>" ...}
|
|
||||||
|
|
||||||
Tips:
|
|
||||||
|
|
||||||
- Test using `with-signal`: (with-signal (catch->error! ...)).
|
|
||||||
- Supports the same options [2] as other signals [1].
|
|
||||||
|
|
||||||
- Useful for recording errors in forms, futures, callbacks, etc.
|
|
||||||
|
|
||||||
See also `error!`.
|
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
|
||||||
[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-flow` - (filters, handling, etc.)
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
Low-level generic signal creator.
|
|
||||||
|
|
||||||
API: [opts] => depends on options [2]
|
|
||||||
Default kind: none (optional)
|
|
||||||
Default level: none (must be provided)
|
|
||||||
|
|
||||||
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.).
|
|
||||||
|
|
||||||
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
|
|
||||||
advanced users writing their own wrapper macros. Regular users will typically
|
|
||||||
prefer one of the higher-level signal creators optimized for ease-of-use in
|
|
||||||
common cases [1].
|
|
||||||
|
|
||||||
Tips:
|
|
||||||
|
|
||||||
- Test using `with-signal`: (with-signal (signal! ...)).
|
|
||||||
- Supports the same options [2] as other signals [1].
|
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
|
||||||
[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-flow` - (filters, handling, etc.)
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
Signals are maps with {:keys [inst id ns level data msg_ ...]},
|
|
||||||
though they can be modified by signal and/or handler middleware.
|
|
||||||
|
|
||||||
Default signal keys:
|
|
||||||
|
|
||||||
`:schema` ------ Int version of signal schema (current: 1)
|
|
||||||
`:inst` -------- Platform instant [1] when signal was created
|
|
||||||
`:level` ------- Signal level ∈ #{<int> :trace :debug :info :warn :error :fatal :report ...}
|
|
||||||
`:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy <user-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 user-level message ?str given to signal creator
|
|
||||||
`:data` -------- Arb user-level data ?val (usu. a map) given to signal creator
|
|
||||||
`:error` ------- Arb user-level platform ?error [2] given to signal creator
|
|
||||||
|
|
||||||
`:run-form` ---- Unevaluated ?form given to signal creator as `:run`
|
|
||||||
`:run-val` ----- Successful return ?val of `:run` ?form
|
|
||||||
`:run-nsecs` --- ?int nanosecs runtime of `:run` ?form
|
|
||||||
`:end-inst` ---- Platform ?instant [1] when `:run` ?form completed
|
|
||||||
|
|
||||||
`:ctx` --------- ?val of `*ctx*` (arb user-level state) when signal was created
|
|
||||||
`:parent` ------ ?{:keys [id uid]} of parent signal, present in nested signals when tracing
|
|
||||||
`: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)
|
|
||||||
`:sample-rate` - ?rate ∈ℝ[0,1] for combined signal AND handler sampling (0.75 => allow 75% of signals, nil => allow all)
|
|
||||||
|
|
||||||
<kvs> ---------- Arb other user-level ?kvs given to signal creator
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
A signal will be provided to a handler iff ALL of the following are true:
|
|
||||||
1. Signal (creation) is allowed by compile-time filters
|
|
||||||
2. Signal (creation) is allowed by runtime filters
|
|
||||||
3. Signal (handling) is allowed by handler filters
|
|
||||||
|
|
||||||
4. Signal middleware does not suppress the signal (return nil)
|
|
||||||
5. Handler middleware does not suppress the signal (return nil)
|
|
||||||
|
|
||||||
For 1-3, filtering may depend on (in order):
|
|
||||||
Sample rate → namespace → kind → id → level → when form/fn → rate limit
|
|
||||||
|
|
||||||
Note that sample rates are multiplicative:
|
|
||||||
If a signal is created with 20% sampling and a handler handles 50%
|
|
||||||
of given signals, then 10% of possible signals will be handled.
|
|
||||||
|
|
||||||
This multiplicative rate is helpfully reflected in each signal's final
|
|
||||||
`:sample-rate` value.
|
|
||||||
|
|
||||||
For a visual flowchart, see: Ref. <https://www.taoensso.com/telemere/flow>
|
|
||||||
|
|
||||||
For more info:
|
|
||||||
- On signal filters, see: `help:signal-filters` docstring
|
|
||||||
- On handler filters, see: `help:signal-handlers` docstring
|
|
||||||
|
|
||||||
If anything is unclear, please ping me (@ptaoussanis) so that I can
|
|
||||||
improve these docs!
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
Common signal formatters include:
|
|
||||||
(utils/format-signal-str->fn) {<opts>}) ; For human-readable string output (default)
|
|
||||||
(utils/format-signal->edn-fn) {<opts>}) ; For edn output
|
|
||||||
(utils/format-signal->json-fn {<opts>}) ; For JSON output
|
|
||||||
|
|
||||||
See relevant docstrings for details.
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
Signal options (shared by all signal creators):
|
|
||||||
|
|
||||||
`:inst` -------- Platform instant [1] when signal was created, ∈ #{nil :auto <user-val>}
|
|
||||||
`:level` ------- Signal level ∈ #{<int> :trace :debug :info :warn :error :fatal :report ...}
|
|
||||||
`:kind` -------- Signal ?kind ∈ #{nil :event :error :log :trace :spy <user-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 user-level ?message to incl. in signal: str or vec of strs to join (with `\space`)
|
|
||||||
`:data` -------- Arb user-level ?data to incl. in signal: usu. a map
|
|
||||||
`:error` ------- Arb user-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 info in signal
|
|
||||||
`: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` -- ?[(fn [signal])=>modified-signal ...] signal middleware
|
|
||||||
`:trace?` ------ Should tracing be enabled for `:run` form?
|
|
||||||
|
|
||||||
<kvs> ---------- Arb other user-level ?kvs to incl. in signal
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
@ -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-flow` - (filters, handling, etc.)
|
|
||||||
|
|
@ -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-flow` - (filters, handling, etc.)
|
|
||||||
1
slf4j/.gitignore
vendored
1
slf4j/.gitignore
vendored
|
|
@ -13,4 +13,3 @@ pom.xml*
|
||||||
/.clj-kondo/.cache
|
/.clj-kondo/.cache
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
/wiki/.git
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(defproject com.taoensso/slf4j-telemere "1.0.0-beta3"
|
(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.2"]
|
[[org.clojure/clojure "1.12.3"]
|
||||||
[org.slf4j/slf4j-api "2.0.13"]
|
[org.slf4j/slf4j-api "2.0.17"]
|
||||||
[com.taoensso/telemere "1.0.0-beta3"]]}
|
[com.taoensso/telemere "1.2.1"]]}
|
||||||
|
|
||||||
:dev
|
:dev
|
||||||
{:plugins
|
{:plugins
|
||||||
|
|
|
||||||
|
|
@ -49,30 +49,30 @@ public class TelemereLogger extends LegacyAbstractLogger implements LoggingEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IFn logFn;
|
private static IFn logFn;
|
||||||
private static IFn isLevelEnabledFn;
|
private static IFn isAllowedFn;
|
||||||
|
|
||||||
static void init() {
|
static void init() {
|
||||||
IFn requireFn = Clojure.var("clojure.core", "require");
|
IFn requireFn = Clojure.var("clojure.core", "require");
|
||||||
requireFn.invoke( Clojure.read("taoensso.telemere.slf4j"));
|
requireFn.invoke(Clojure.read("taoensso.telemere.slf4j"));
|
||||||
|
isAllowedFn = Clojure.var("taoensso.telemere.slf4j", "allowed?");
|
||||||
logFn = Clojure.var("taoensso.telemere.slf4j", "log!");
|
logFn = Clojure.var("taoensso.telemere.slf4j", "log!");
|
||||||
isLevelEnabledFn = Clojure.var("taoensso.telemere.slf4j", "allowed?");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected TelemereLogger(String name) { this.name = name; }
|
protected TelemereLogger(String name) { this.name = name; }
|
||||||
|
|
||||||
protected boolean isLevelEnabled(Level level) { return (boolean) isLevelEnabledFn.invoke(level); }
|
protected boolean isLevelEnabled(Level level) { return (boolean) isAllowedFn.invoke(this.name, level); }
|
||||||
public boolean isTraceEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.TRACE); }
|
public boolean isTraceEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.TRACE); }
|
||||||
public boolean isDebugEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.DEBUG); }
|
public boolean isDebugEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.DEBUG); }
|
||||||
public boolean isInfoEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.INFO); }
|
public boolean isInfoEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.INFO); }
|
||||||
public boolean isWarnEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.WARN); }
|
public boolean isWarnEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.WARN); }
|
||||||
public boolean isErrorEnabled() { return (boolean) isLevelEnabledFn.invoke(Level.ERROR); }
|
public boolean isErrorEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.ERROR); }
|
||||||
|
|
||||||
public void log(LoggingEvent event) { logFn.invoke(event); } // Fluent (modern) API, called after level check
|
public void log(LoggingEvent event) { logFn.invoke(this.name, event); } // Fluent (modern) API, called after level check
|
||||||
|
|
||||||
@Override protected String getFullyQualifiedCallerName() { return null; }
|
@Override protected String getFullyQualifiedCallerName() { return null; }
|
||||||
@Override
|
@Override
|
||||||
protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) {
|
protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) {
|
||||||
logFn.invoke(level, throwable, messagePattern, arguments, marker); // Legacy API, called after level check
|
logFn.invoke(this.name, level, throwable, messagePattern, arguments, marker); // Legacy API, called after level check
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,36 +97,33 @@
|
||||||
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]
|
||||||
[^org.slf4j.event.Level level]
|
(when-debug (println [:slf4j/allowed? (sig-level level) logger-name]))
|
||||||
(when-debug (println [:slf4j/allowed? (sig-level level)]))
|
|
||||||
(impl/signal-allowed?
|
(impl/signal-allowed?
|
||||||
{:location nil
|
{:ns logger-name ; Typically source class name
|
||||||
:kind :log
|
:kind :slf4j
|
||||||
:id :taoensso.telemere/slf4j
|
|
||||||
:level (sig-level level)}))
|
:level (sig-level level)}))
|
||||||
|
|
||||||
(defn- normalized-log!
|
(defn- normalized-log!
|
||||||
[inst level error msg-pattern args marker-names kvs]
|
[logger-name level inst error msg-pattern args marker-names kvs]
|
||||||
(when-debug (println [:slf4j/normalized-log! (sig-level level)]))
|
(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 nil
|
:ns logger-name ; Typically source class name
|
||||||
:kind :log
|
:kind :slf4j
|
||||||
:id :taoensso.telemere/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))
|
||||||
|
|
||||||
|
|
@ -131,42 +132,40 @@
|
||||||
(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
|
||||||
([^org.slf4j.event.LoggingEvent event]
|
([logger-name ^org.slf4j.event.LoggingEvent event]
|
||||||
(let [inst (or (when-let [ts (.getTimeStamp event)] (java.time.Instant/ofEpochMilli ts)) (enc/now-inst*))
|
(let [inst (or (when-let [ts (.getTimeStamp event)] (java.time.Instant/ofEpochMilli ts)) (enc/now-inst*))
|
||||||
level (.getLevel event)
|
level (.getLevel event)
|
||||||
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]
|
||||||
(assoc acc (.-key kvp) (.-value kvp)))
|
(assoc acc (.-key kvp) (.-value kvp)))
|
||||||
nil kvps))]
|
nil kvps))]
|
||||||
|
|
||||||
(when-debug (println [:slf4j/fluent-log-call (sig-level level)]))
|
(when-debug (println [:slf4j/fluent-log-call (sig-level level) logger-name]))
|
||||||
(normalized-log! inst level 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
|
||||||
([^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)]))
|
(when-debug (println [:slf4j/legacy-log-call (sig-level level) logger-name]))
|
||||||
(normalized-log! (enc/now-inst*) level error msg-pattern args marker-names nil))))
|
(normalized-log! logger-name level (enc/now-inst*) error msg-pattern args marker-names nil))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MySlfLogger"))
|
(def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "my.class"))
|
||||||
(impl/with-signal (-> sl (.info "Hello {}" "x")))
|
(impl/with-signal (-> sl (.info "Hello {}" "x")))
|
||||||
(impl/with-signal (-> (.atInfo sl) (.log "Hello {}" "x")))
|
(impl/with-signal (-> (.atInfo sl) (.log "Hello {}" "x")))
|
||||||
|
|
||||||
|
|
@ -178,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 :info
|
: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"}))
|
||||||
|
|
|
||||||
|
|
@ -1,437 +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]
|
|
||||||
#?(:clj [taoensso.telemere.streams :as streams])
|
|
||||||
#?(:default [taoensso.telemere.console-handlers :as ch])
|
|
||||||
#?(:clj [taoensso.telemere.file-handler :as fh]))
|
|
||||||
|
|
||||||
#?(:cljs
|
|
||||||
(:require-macros
|
|
||||||
[taoensso.telemere :refer
|
|
||||||
[set-ctx! with-ctx with-ctx+
|
|
||||||
set-middleware! with-middleware
|
|
||||||
|
|
||||||
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+]])))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(remove-ns 'taoensso.telemere)
|
|
||||||
(:api (enc/interns-overview)))
|
|
||||||
|
|
||||||
(enc/assert-min-encore-version [3 104 1])
|
|
||||||
|
|
||||||
;;;; TODO
|
|
||||||
;; - Add email handler
|
|
||||||
;; - Native OpenTelemetry traces and spans
|
|
||||||
;; - 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
|
|
||||||
{:purpose "signal"
|
|
||||||
:sf-arity 4
|
|
||||||
:ct-sig-filter impl/ct-sig-filter
|
|
||||||
:*rt-sig-filter* impl/*rt-sig-filter*
|
|
||||||
:*sig-handlers* impl/*sig-handlers*
|
|
||||||
:sig-filter-system-vals-info
|
|
||||||
"These include:
|
|
||||||
|
|
||||||
Compile-time:
|
|
||||||
|
|
||||||
ns-filter: (get-env {:as :edn} :taoensso.telemere/ct-ns-filter<.platform><.edn>)
|
|
||||||
id-filter: (get-env {:as :edn} :taoensso.telemere/ct-id-filter<.platform><.edn>)
|
|
||||||
min-level: (get-env {:as :edn} :taoensso.telemere/ct-min-level<.platform><.edn>)
|
|
||||||
|
|
||||||
Runtime:
|
|
||||||
|
|
||||||
ns-filter: (get-env {:as :edn} :taoensso.telemere/rt-ns-filter<.platform><.edn>)
|
|
||||||
id-filter: (get-env {:as :edn} :taoensso.telemere/rt-id-filter<.platform><.edn>)
|
|
||||||
min-level: (get-env {:as :edn, :default :info} :taoensso.telemere/rt-min-level<.platform><.edn>)
|
|
||||||
|
|
||||||
See `get-env` for details."})
|
|
||||||
|
|
||||||
(comment help:filters)
|
|
||||||
|
|
||||||
(comment
|
|
||||||
[level-aliases
|
|
||||||
|
|
||||||
help:handlers get-handlers add-handler! remove-handler!
|
|
||||||
with-handler with-handler+ shut-down-handlers!
|
|
||||||
|
|
||||||
help:filtering get-filters get-min-level
|
|
||||||
set-kind-filter! set-ns-filter! set-id-filter! set-min-level!
|
|
||||||
with-kind-filter with-ns-filter with-id-filter with-min-level])
|
|
||||||
|
|
||||||
;;;; Aliases
|
|
||||||
|
|
||||||
(enc/defaliases
|
|
||||||
#?(:clj enc/set-var-root!)
|
|
||||||
#?(:clj enc/update-var-root!)
|
|
||||||
#?(:clj enc/get-env)
|
|
||||||
enc/chance
|
|
||||||
enc/rate-limiter
|
|
||||||
enc/newline
|
|
||||||
|
|
||||||
impl/msg-splice
|
|
||||||
impl/msg-skip
|
|
||||||
#?(:clj impl/with-signal)
|
|
||||||
#?(:clj impl/with-signals)
|
|
||||||
#?(:clj impl/signal!)
|
|
||||||
utils/error-signal?)
|
|
||||||
|
|
||||||
;;;; Help
|
|
||||||
|
|
||||||
(impl/defhelp help:signal-creators :signal-creators)
|
|
||||||
(impl/defhelp help:signal-options :signal-options)
|
|
||||||
(impl/defhelp help:signal-flow :signal-flow)
|
|
||||||
(impl/defhelp help:signal-content :signal-content)
|
|
||||||
(enc/defalias help:signal-filters help:filters) ; Via Encore
|
|
||||||
(enc/defalias help:signal-handlers help:handlers) ; Via Encore
|
|
||||||
(impl/defhelp help:signal-formatters :signal-formatters)
|
|
||||||
|
|
||||||
;;;; Context
|
|
||||||
|
|
||||||
(enc/defonce default-ctx
|
|
||||||
"Default root (base) value of `*ctx*` var.
|
|
||||||
Defaults to `nil`, controlled by:
|
|
||||||
(get-env {:as :edn} :taoensso.telemere/default-ctx<.platform><.edn>)
|
|
||||||
|
|
||||||
See `get-env` for details."
|
|
||||||
(enc/get-env {:as :edn} :taoensso.telemere/default-ctx<.platform><.edn>))
|
|
||||||
|
|
||||||
(enc/def* ^:dynamic *ctx*
|
|
||||||
"Dynamic context: arbitrary user-level state attached as `:ctx` to all signals.
|
|
||||||
Value may be any type, but is usually nil or a map.
|
|
||||||
|
|
||||||
Re/bind dynamic value using `with-ctx`, `with-ctx+`, or `binding`.
|
|
||||||
Modify root (base) value using `set-ctx!`.
|
|
||||||
Default root (base) value is `default-ctx`.
|
|
||||||
|
|
||||||
Note that as with all dynamic Clojure vars, \"binding conveyance\" applies
|
|
||||||
when using futures, agents, etc.
|
|
||||||
|
|
||||||
Tips:
|
|
||||||
- Value may be (or may contain) an atom if you want mutable semantics
|
|
||||||
- Value may be of form {<scope-id> <data>} for custom scoping, etc."
|
|
||||||
default-ctx)
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro set-ctx!
|
|
||||||
"Set `*ctx*` var's root (base) value. See `*ctx*` for details."
|
|
||||||
[root-val] `(enc/set-var-root! *ctx* ~root-val)))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro with-ctx
|
|
||||||
"Evaluates given form with given `*ctx*` value. See `*ctx*` for details."
|
|
||||||
[init-val form] `(binding [*ctx* ~init-val] ~form)))
|
|
||||||
|
|
||||||
(comment (with-ctx "my-ctx" *ctx*))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro with-ctx+
|
|
||||||
"Evaluates given form with updated `*ctx*` value.
|
|
||||||
|
|
||||||
`update-map-or-fn` may be:
|
|
||||||
- A map to merge with current `*ctx*` value, or
|
|
||||||
- A unary fn to apply to current `*ctx*` value
|
|
||||||
|
|
||||||
See `*ctx*` for details."
|
|
||||||
[update-map-or-fn form]
|
|
||||||
`(binding [*ctx* (impl/update-ctx *ctx* ~update-map-or-fn)]
|
|
||||||
~form)))
|
|
||||||
|
|
||||||
(comment (with-ctx {:a :A1 :b :B1} (with-ctx+ {:a :A2} *ctx*)))
|
|
||||||
|
|
||||||
;;;; Middleware
|
|
||||||
|
|
||||||
(enc/defonce ^:dynamic *middleware*
|
|
||||||
"Optional vector of unary middleware fns to apply (sequentially/left-to-right)
|
|
||||||
to each signal before passing it to handlers. If any middleware fn returns nil,
|
|
||||||
aborts immediately without calling handlers.
|
|
||||||
|
|
||||||
Useful for transforming each signal before handling.
|
|
||||||
|
|
||||||
Re/bind dynamic value using `with-middleware`, `binding`.
|
|
||||||
Modify root (base) value using `set-middleware!`."
|
|
||||||
nil)
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro set-middleware!
|
|
||||||
"Set `*middleware*` var's root (base) value. See `*middleware*` for details."
|
|
||||||
[root-val] `(enc/set-var-root! *middleware* ~root-val)))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro with-middleware
|
|
||||||
"Evaluates given form with given `*middleware*` value.
|
|
||||||
See `*middleware*` for details."
|
|
||||||
[init-val form] `(binding [*middleware* ~init-val] ~form)))
|
|
||||||
|
|
||||||
;;;; Encore integration
|
|
||||||
|
|
||||||
(do
|
|
||||||
(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}))))
|
|
||||||
|
|
||||||
;;;; Signal creators
|
|
||||||
;; - signal! [ opts] ; => allowed? / run result (value or throw)
|
|
||||||
;; - event! [id ] [id level-or-opts] ; id + ?level => allowed? ; Sole signal with descending main arg!
|
|
||||||
;; - log! [msg ] [level-or-opts msg] ; msg + ?level => allowed?
|
|
||||||
;; - error! [error] [id-or-opts error] ; error + ?id => given error
|
|
||||||
;; - trace! [form ] [id-or-opts form] ; run + ?id => run result (value or throw)
|
|
||||||
;; - spy! [form ] [level-or-opts form] ; run + ?level => run result (value or throw)
|
|
||||||
;; - catch->error! [form ] [id-or-opts form] ; run + ?id => run value or ?return
|
|
||||||
;; - uncaught->error! [ ] [id-or-opts ] ; ?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 ch/handler:console)
|
|
||||||
#?(:cljs ch/handler:console-raw)
|
|
||||||
#?(:clj fh/handler:file))
|
|
||||||
|
|
||||||
;;;; Flow benchmarks
|
|
||||||
|
|
||||||
(comment
|
|
||||||
{:last-updated "2024-02-12"
|
|
||||||
:system "2020 Macbook Pro M1, 16 GB memory"
|
|
||||||
:clojure-version "1.11.1"
|
|
||||||
:java-version "OpenJDK 21"}
|
|
||||||
|
|
||||||
[(binding [impl/*sig-handlers* nil]
|
|
||||||
(enc/qb 1e6 ; [10.4 17.06 195.42 200.34]
|
|
||||||
(signal! {:level :info, :run nil, :elide? true})
|
|
||||||
(signal! {:level :info, :run nil, :allow? false})
|
|
||||||
(signal! {:level :info, :run nil, :allow? true })
|
|
||||||
(signal! {:level :info, :run nil})))
|
|
||||||
|
|
||||||
(binding [impl/*sig-handlers* nil]
|
|
||||||
(enc/qb 1e6 ; [8.1 15.35 647.82 279.67 682.1]
|
|
||||||
(signal! {:level :info, :run "run", :elide? true})
|
|
||||||
(signal! {:level :info, :run "run", :allow? false})
|
|
||||||
(signal! {:level :info, :run "run", :allow? true })
|
|
||||||
(signal! {:level :info, :run "run", :trace? false})
|
|
||||||
(signal! {:level :info, :run "run"})))
|
|
||||||
|
|
||||||
;; For README "performance" table
|
|
||||||
(binding [impl/*sig-handlers* nil]
|
|
||||||
(enc/qb [8 1e6] ; [9.23 197.2 277.55 649.32]
|
|
||||||
(signal! {:level :info, :elide? true})
|
|
||||||
(signal! {:level :info})
|
|
||||||
(signal! {:level :info, :run "run", :trace? false})
|
|
||||||
(signal! {:level :info, :run "run"})))])
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(impl/on-init
|
|
||||||
(add-handler! :default/console (handler:console))
|
|
||||||
|
|
||||||
#?(:clj (enc/catching (require '[taoensso.telemere.tools-logging])))
|
|
||||||
#?(:clj (enc/catching (require '[taoensso.telemere.slf4j]))))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(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)))))
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
(ns ^:no-doc taoensso.telemere.console-handlers
|
|
||||||
"Private ns, implementation detail.
|
|
||||||
Core console handlers."
|
|
||||||
(:require
|
|
||||||
[taoensso.encore :as enc :refer [have have?]]
|
|
||||||
[taoensso.telemere.utils :as utils]))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(remove-ns 'taoensso.telemere.console-handlers)
|
|
||||||
(:api (enc/interns-overview)))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn ^:public handler:console
|
|
||||||
"Experimental, subject to change.
|
|
||||||
|
|
||||||
Returns a (fn handler [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Writes a formatted signal string to stream.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`
|
|
||||||
|
|
||||||
`:stream` - `java.io.writer`
|
|
||||||
Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise."
|
|
||||||
|
|
||||||
([] (handler:console nil))
|
|
||||||
([{:keys [format-signal-fn stream]
|
|
||||||
:or {format-signal-fn (utils/format-signal->str-fn)}}]
|
|
||||||
|
|
||||||
(let [stream (case stream :*out* *out*, :*err* *err* stream)
|
|
||||||
error-signal? utils/error-signal?
|
|
||||||
nl utils/newline]
|
|
||||||
|
|
||||||
(fn a-handler:console
|
|
||||||
([]) ; Shut down (no-op)
|
|
||||||
([signal]
|
|
||||||
(let [^java.io.Writer stream
|
|
||||||
(or stream (if (error-signal? signal) *err* *out*))]
|
|
||||||
(when-let [output (format-signal-fn signal)]
|
|
||||||
(.write stream (str output nl))
|
|
||||||
(.flush stream))))))))
|
|
||||||
|
|
||||||
:cljs
|
|
||||||
(defn ^:public handler:console
|
|
||||||
"Experimental, subject to change.
|
|
||||||
|
|
||||||
If `js/console` exists, returns a (fn handler [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Writes a formatted signal string to JavaScript console.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`"
|
|
||||||
|
|
||||||
([] (handler:console nil))
|
|
||||||
([{:keys [format-signal-fn]
|
|
||||||
:or {format-signal-fn (utils/format-signal->str-fn)}}]
|
|
||||||
|
|
||||||
(when (exists? js/console)
|
|
||||||
(let [js-console-logger utils/js-console-logger
|
|
||||||
nl utils/newline]
|
|
||||||
|
|
||||||
(fn a-handler:console
|
|
||||||
([]) ; Shut down (no-op)
|
|
||||||
([signal]
|
|
||||||
(when-let [output (format-signal-fn signal)]
|
|
||||||
(let [logger (js-console-logger (get signal :level))]
|
|
||||||
(.call logger logger (str output nl)))))))))))
|
|
||||||
|
|
||||||
#?(:cljs
|
|
||||||
(defn- logger-fn [logger]
|
|
||||||
;; (fn [& xs] (.apply logger logger (into-array xs)))
|
|
||||||
(fn
|
|
||||||
([x1 ] (.call logger logger x1))
|
|
||||||
([x1 x2 ] (.call logger logger x1 x2))
|
|
||||||
([x1 x2 x3 ] (.call logger logger x1 x2 x3))
|
|
||||||
([x1 x2 x3 & more] (apply logger x1 x2 x3 more)))))
|
|
||||||
|
|
||||||
#?(:cljs
|
|
||||||
(defn ^:public handler:console-raw
|
|
||||||
"Experimental, subject to change.
|
|
||||||
|
|
||||||
If `js/console` exists, returns a (fn handler [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Writes raw signal data to JavaScript console.
|
|
||||||
|
|
||||||
Intended for use with browser formatting tools like `binaryage/devtools`,
|
|
||||||
Ref. <https://github.com/binaryage/cljs-devtools>."
|
|
||||||
|
|
||||||
([] (handler:console-raw nil))
|
|
||||||
([{:keys [format-signal-prelude-fn format-nsecs-fn] :as opts
|
|
||||||
:or
|
|
||||||
{format-signal-prelude-fn (utils/format-signal-prelude-fn) ; (fn [signal])
|
|
||||||
format-nsecs-fn (utils/format-nsecs-fn) ; (fn [nanosecs])
|
|
||||||
}}]
|
|
||||||
|
|
||||||
(when (and (exists? js/console) (exists? js/console.group))
|
|
||||||
(let [js-console-logger utils/js-console-logger
|
|
||||||
signal-content-handler ; (fn [signal hf vf]
|
|
||||||
(utils/signal-content-handler
|
|
||||||
{:format-nsecs-fn format-nsecs-fn
|
|
||||||
:format-error-fn nil
|
|
||||||
:raw-error? true})]
|
|
||||||
|
|
||||||
(fn a-handler:console-raw
|
|
||||||
([]) ; Shut down (no-op)
|
|
||||||
([signal]
|
|
||||||
(let [{:keys [level error]} signal
|
|
||||||
logger (js-console-logger level)]
|
|
||||||
|
|
||||||
;; Unfortunately groups have no level
|
|
||||||
(.group js/console (format-signal-prelude-fn signal))
|
|
||||||
(signal-content-handler signal (logger-fn logger) identity)
|
|
||||||
|
|
||||||
(when-let [stack (and error (.-stack (enc/ex-root error)))]
|
|
||||||
(.call logger logger stack))
|
|
||||||
|
|
||||||
(.groupEnd js/console)))))))))
|
|
||||||
|
|
@ -1,727 +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*))
|
|
||||||
|
|
||||||
;;;; Utils
|
|
||||||
|
|
||||||
#?(:clj (defmacro threaded [& body] `(let [t# (Thread. (fn [] ~@body))] (.start t#) t#)))
|
|
||||||
|
|
||||||
#?(: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>)
|
|
||||||
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
|
|
||||||
{: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>)
|
|
||||||
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
|
|
||||||
{: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))})))
|
|
||||||
|
|
||||||
;;;; Context (optional arb app-level state)
|
|
||||||
;; taoensso.telemere/*ctx*
|
|
||||||
|
|
||||||
(defn update-ctx
|
|
||||||
"Returns `new-ctx` given `old-ctx` and an update map or fn."
|
|
||||||
[old-ctx update-map-or-fn]
|
|
||||||
(enc/cond
|
|
||||||
(nil? update-map-or-fn) old-ctx
|
|
||||||
(map? update-map-or-fn) (enc/fast-merge old-ctx update-map-or-fn) ; Before ifn
|
|
||||||
(ifn? update-map-or-fn) (update-map-or-fn old-ctx)
|
|
||||||
:else
|
|
||||||
(enc/unexpected-arg! update-map-or-fn
|
|
||||||
{:context `update-ctx
|
|
||||||
:param 'update-map-or-fn
|
|
||||||
:expected '#{nil map fn}})))
|
|
||||||
|
|
||||||
;;;; Unique IDs (UIDs)
|
|
||||||
|
|
||||||
(enc/def* nanoid-readable (enc/rand-id-fn {:chars :nanoid-readable, :len 23}))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn- parse-uid-form [uid-form]
|
|
||||||
(when uid-form
|
|
||||||
(case uid-form
|
|
||||||
:auto/uuid `(enc/uuid)
|
|
||||||
:auto/uuid-str `(enc/uuid-str)
|
|
||||||
:auto/nanoid `(enc/nanoid)
|
|
||||||
:auto/nanoid-readable `(nanoid-readable)
|
|
||||||
uid-form))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(enc/qb 1e6 ; [161.36 184.69 274.53 468.67]
|
|
||||||
(enc/uuid)
|
|
||||||
(enc/uuid-str)
|
|
||||||
(enc/nanoid)
|
|
||||||
(nanoid-readable)))
|
|
||||||
|
|
||||||
;;;; Messages
|
|
||||||
|
|
||||||
(deftype MsgSkip [])
|
|
||||||
(deftype MsgSplice [args])
|
|
||||||
|
|
||||||
(def ^:public msg-skip
|
|
||||||
"For use within signal message vectors.
|
|
||||||
Special value that will be ignored (no-op) 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-parent* "?TraceParent" nil)
|
|
||||||
(defrecord TraceParent [id uid])
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro with-tracing
|
|
||||||
"Wraps `form` with tracing iff const boolean `trace?` is true."
|
|
||||||
[trace? id uid form]
|
|
||||||
|
|
||||||
;; Not much motivation to support runtime `trace?` form, but easy
|
|
||||||
;; to add support later if desired
|
|
||||||
(when-not (enc/const-form? trace?)
|
|
||||||
(enc/unexpected-arg! trace?
|
|
||||||
{:msg "Expected constant (compile-time) `:trace?` value"
|
|
||||||
:context `with-tracing}))
|
|
||||||
|
|
||||||
(if trace?
|
|
||||||
`(binding [*trace-parent* (TraceParent. ~id ~uid)] ~form)
|
|
||||||
(do form))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
[(enc/qb 1e6 (with-tracing true :id1 :uid1 "form")) ; 257.5
|
|
||||||
(macroexpand '(with-tracing false :id1 :uid1 "form"))
|
|
||||||
(macroexpand '(with-tracing true :id1 :uid1 "form"))])
|
|
||||||
|
|
||||||
;;;; Main types
|
|
||||||
|
|
||||||
(defrecord Signal
|
|
||||||
;; Telemere's main public data type, we avoid nesting and duplication
|
|
||||||
[^long schema inst uid,
|
|
||||||
location ns line column file,
|
|
||||||
sample-rate, kind id level, ctx parent,
|
|
||||||
data msg_ error run-form run-val,
|
|
||||||
end-inst run-nsecs kvs]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
(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 ns kind id level))
|
|
||||||
(signal-value [_ handler-context]
|
|
||||||
(let [sig-val @signal-value_]
|
|
||||||
(or
|
|
||||||
(when-let [handler-sample-rate
|
|
||||||
(when-let [^taoensso.encore.signals.HandlerContext hc handler-context]
|
|
||||||
(.-sample-rate hc))]
|
|
||||||
|
|
||||||
(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.
|
|
||||||
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.
|
|
||||||
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 constructor
|
|
||||||
|
|
||||||
(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 new-signal
|
|
||||||
"Returns a new `Signal` with given opts."
|
|
||||||
^Signal
|
|
||||||
;; Note all dynamic vals passed as explicit args for better control
|
|
||||||
[inst uid,
|
|
||||||
location ns line column file,
|
|
||||||
sample-rate, kind id level, ctx parent,
|
|
||||||
kvs data msg_,
|
|
||||||
run-form run-result error]
|
|
||||||
|
|
||||||
(let [signal
|
|
||||||
(if-let [^RunResult run-result run-result]
|
|
||||||
(let [run-nsecs (.-run-nsecs run-result)
|
|
||||||
end-inst
|
|
||||||
#?(:clj (.plusNanos ^java.time.Instant inst run-nsecs)
|
|
||||||
:cljs (js/Date. (+ (.getTime inst) (/ run-nsecs 1e6))))
|
|
||||||
|
|
||||||
run-err (.-error run-result)
|
|
||||||
run-val (.-value run-result)
|
|
||||||
msg_
|
|
||||||
(if (fn? msg_) ; Undocumented, handy for `trace!`/`spy!`, etc.
|
|
||||||
(delay (msg_ run-form run-val run-err run-nsecs))
|
|
||||||
msg_)]
|
|
||||||
|
|
||||||
(Signal. 1 inst uid,
|
|
||||||
location ns line column file,
|
|
||||||
sample-rate, kind id level, ctx parent,
|
|
||||||
data msg_,
|
|
||||||
run-err run-form run-val,
|
|
||||||
end-inst run-nsecs kvs))
|
|
||||||
|
|
||||||
(Signal. 1 inst uid,
|
|
||||||
location ns line column file,
|
|
||||||
sample-rate, kind id level, ctx parent,
|
|
||||||
data msg_, error nil nil nil nil kvs))]
|
|
||||||
|
|
||||||
(if kvs
|
|
||||||
(reduce-kv assoc signal kvs)
|
|
||||||
(do signal))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(enc/qb 1e6 ; 55.67
|
|
||||||
(new-signal
|
|
||||||
nil nil nil nil nil nil nil nil nil nil
|
|
||||||
nil nil nil nil nil nil nil nil nil)))
|
|
||||||
|
|
||||||
;;;; 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 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 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 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 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 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 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 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
|
|
||||||
|
|
||||||
#?(: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))
|
|
||||||
{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))
|
|
||||||
|
|
||||||
inst-form (get opts :inst :auto)
|
|
||||||
inst-form (if (= inst-form :auto) `(enc/now-inst*) inst-form)
|
|
||||||
|
|
||||||
uid-form (get opts :uid (when trace? :auto/uuid))
|
|
||||||
uid-form (parse-uid-form uid-form)
|
|
||||||
|
|
||||||
signal-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*)
|
|
||||||
parent-form (get opts :parent (when trace? `taoensso.telemere.impl/*trace-parent*))
|
|
||||||
|
|
||||||
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))]
|
|
||||||
|
|
||||||
(when (and run-form error-form)
|
|
||||||
(throw ; Prevent ambiguity re: source of error
|
|
||||||
(ex-info "Signals cannot have both `:run` and `:error` opts at the same time"
|
|
||||||
{:run-form run-form
|
|
||||||
:error-form error-form
|
|
||||||
:location location
|
|
||||||
:other-opts (dissoc opts :run :error)})))
|
|
||||||
|
|
||||||
;; Eval let bindings AFTER call filtering but BEFORE data, msg
|
|
||||||
`(do
|
|
||||||
~do-form
|
|
||||||
(let ~let-form ; Allow to throw during `signal-value_` deref
|
|
||||||
(new-signal ~'__inst ~'__uid
|
|
||||||
~location ~'__ns ~line-form ~column-form ~file-form,
|
|
||||||
~sample-rate-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form,
|
|
||||||
~kvs-form ~data-form ~msg-form,
|
|
||||||
'~run-form ~'__run-result ~error-form))))
|
|
||||||
|
|
||||||
run-fn-form (when run-form `(fn [] (~run-form)))]
|
|
||||||
|
|
||||||
;; Could avoid double `run-form` expansion with a fn wrap (>0 cost)
|
|
||||||
;; `(let [~'run-fn-form ~run-fn-form]
|
|
||||||
;; (if-not ~allow?
|
|
||||||
;; (run-fn-form)
|
|
||||||
;; (let [...])))
|
|
||||||
|
|
||||||
`(enc/if-not ~allow? ; Allow to throw at call
|
|
||||||
~run-form
|
|
||||||
(let [~'__inst ~inst-form ; Allow to throw at call
|
|
||||||
~'__level ~level-form ; ''
|
|
||||||
~'__kind ~kind-form ; ''
|
|
||||||
~'__id ~id-form ; ''
|
|
||||||
~'__uid ~uid-form ; ''
|
|
||||||
~'__ns ~ns-form ; ''
|
|
||||||
|
|
||||||
~'__call-middleware ~(get opts :middleware `taoensso.telemere/*middleware*)
|
|
||||||
~'__run-result ; Non-throwing (traps)
|
|
||||||
~(when run-form
|
|
||||||
`(let [~'__t0 (enc/now-nano*)]
|
|
||||||
(with-tracing ~trace? ~'__id ~'__uid
|
|
||||||
(enc/try*
|
|
||||||
(do (RunResult. ~run-form nil (- (enc/now-nano*) ~'__t0)))
|
|
||||||
(catch :all ~'__t (RunResult. nil ~'__t (- (enc/now-nano*) ~'__t0)))))))
|
|
||||||
|
|
||||||
~'__signal_
|
|
||||||
(delay
|
|
||||||
;; Cache shared by all handlers. Covers signal `:let` eval, signal construction,
|
|
||||||
;; middleware (possibly expensive), etc.
|
|
||||||
|
|
||||||
;; The unwrapped signal value actually visible to users/handler-fns, realized only
|
|
||||||
;; AFTER handler filtering. Allowed to throw on deref (handler will catch).
|
|
||||||
(let [~'__signal ~signal-form] ; Can throw
|
|
||||||
(if ~'__call-middleware
|
|
||||||
((sigs/get-middleware-fn ~'__call-middleware) ~'__signal) ; Can throw
|
|
||||||
(do ~'__signal))))]
|
|
||||||
|
|
||||||
;; Unconditionally send same wrapped signal to all handlers.
|
|
||||||
;; Each handler will then use wrapper for filtering, unwrapping allowed signals.
|
|
||||||
(dispatch-signal! (WrappedSignal. ~'__ns ~'__kind ~'__id ~'__level ~'__signal_))
|
|
||||||
|
|
||||||
(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, `clojure.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?))))
|
|
||||||
|
|
||||||
;;;; Intake
|
|
||||||
|
|
||||||
#?(: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)))))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
(ns taoensso.telemere.open-telemetry
|
|
||||||
"Core OpenTelemetry handler and utils.
|
|
||||||
|
|
||||||
Needs `OpenTelemetry Java`,
|
|
||||||
Ref. <https://github.com/open-telemetry/opentelemetry-java>."
|
|
||||||
|
|
||||||
(:require
|
|
||||||
[clojure.string :as str]
|
|
||||||
[taoensso.encore :as enc :refer [have have?]]
|
|
||||||
[taoensso.telemere.utils :as utils]
|
|
||||||
[taoensso.telemere.impl :as impl]
|
|
||||||
[taoensso.telemere :as tel])
|
|
||||||
|
|
||||||
(:import
|
|
||||||
[io.opentelemetry.api.logs LoggerProvider Severity]
|
|
||||||
[io.opentelemetry.api.common Attributes AttributesBuilder]
|
|
||||||
[io.opentelemetry.api GlobalOpenTelemetry]))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(remove-ns 'taoensso.telemere.open-telemetry)
|
|
||||||
(:api (enc/interns-overview)))
|
|
||||||
|
|
||||||
;;;; Implementation
|
|
||||||
|
|
||||||
(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))
|
|
||||||
|
|
||||||
(def ^:private ^String attr-name
|
|
||||||
"Returns cached OpenTelemetry-style name: `:foo/bar-baz` -> \"foo_bar_baz\", etc.
|
|
||||||
Ref. <https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/>."
|
|
||||||
(enc/fmemoize
|
|
||||||
(fn
|
|
||||||
([prefix x] (str (attr-name prefix) "." (attr-name x))) ; For `merge-prefix-map`, etc.
|
|
||||||
([ 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 :x1.x2/x3-x4 :Foo/Bar-BAZ))) ; 63.6
|
|
||||||
|
|
||||||
;; AttributeTypes: String, Long, Double, Boolean, and arrays
|
|
||||||
(defprotocol IAttr+ (^:private attr+ [_aval akey builder]))
|
|
||||||
(extend-protocol IAttr+
|
|
||||||
nil (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) "nil")) ; Like pr-edn*
|
|
||||||
Boolean (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
|
|
||||||
|
|
||||||
String (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
|
|
||||||
clojure.lang.Named (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v str))) ; ":foo/bar", etc.
|
|
||||||
java.util.UUID (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v str))) ; "d4fc65a0..."
|
|
||||||
|
|
||||||
Long (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
|
|
||||||
Integer (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v long)))
|
|
||||||
Short (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v long)))
|
|
||||||
Byte (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v long)))
|
|
||||||
Double (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
|
|
||||||
Float (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v double)))
|
|
||||||
Number (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (-> v double)))
|
|
||||||
|
|
||||||
clojure.lang.IPersistentCollection
|
|
||||||
(attr+ [v k ^AttributesBuilder b]
|
|
||||||
(let [v1 (first v)]
|
|
||||||
(or
|
|
||||||
(cond
|
|
||||||
(boolean? v1) (enc/catching :common (.put b (attr-name k) (boolean-array (mapv boolean v))))
|
|
||||||
(int? v1) (enc/catching :common (.put b (attr-name k) (long-array (mapv long v))))
|
|
||||||
(float? v1) (enc/catching :common (.put b (attr-name k) (double-array (mapv double v)))))
|
|
||||||
(do (.put b (attr-name k) ^"[Ljava.lang.String;" (into-array String (mapv enc/pr-edn* v)))))))
|
|
||||||
|
|
||||||
Object (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (enc/pr-edn* v))))
|
|
||||||
|
|
||||||
(defn- as-attrs
|
|
||||||
"Returns `io.opentelemetry.api.common.Attributes` for given map."
|
|
||||||
^Attributes [m]
|
|
||||||
(if (empty? m)
|
|
||||||
(Attributes/empty)
|
|
||||||
(let [builder (Attributes/builder)]
|
|
||||||
(enc/run-kv! (fn [k v] (attr+ v k builder)) m)
|
|
||||||
(.build builder))))
|
|
||||||
|
|
||||||
(comment (str (as-attrs {:s "s", :kw :foo/bar, :long 5, :double 5.0, :longs [5 5 5] :nil nil})))
|
|
||||||
|
|
||||||
(defn- merge-prefix-map
|
|
||||||
"Merges prefixed `from` into `to`."
|
|
||||||
[to prefix from]
|
|
||||||
(enc/cond
|
|
||||||
(map? from)
|
|
||||||
(reduce-kv
|
|
||||||
(fn [acc k v] (assoc acc (attr-name prefix k) v))
|
|
||||||
to from)
|
|
||||||
|
|
||||||
from (assoc to prefix from)
|
|
||||||
:else to))
|
|
||||||
|
|
||||||
(comment (merge-prefix-map {} "data" {:a/b1 "v1" :a/b2 "v2" :nil nil}))
|
|
||||||
|
|
||||||
(defn- signal->attrs-map
|
|
||||||
"Returns attributes map for given signal,
|
|
||||||
Ref. <https://opentelemetry.io/docs/specs/otel/logs/data-model/>."
|
|
||||||
[attrs-key signal]
|
|
||||||
(let [attrs-map
|
|
||||||
(let [{:keys [ns line file, kind level id uid parent,
|
|
||||||
run-form run-val run-nsecs, sample-rate]}
|
|
||||||
signal]
|
|
||||||
|
|
||||||
(enc/assoc-some nil
|
|
||||||
{"ns" ns
|
|
||||||
"line" line
|
|
||||||
"file" file
|
|
||||||
|
|
||||||
"error" (utils/error-signal? signal) ; Standard key
|
|
||||||
"kind" kind
|
|
||||||
"level" level
|
|
||||||
"id" id
|
|
||||||
"uid" uid
|
|
||||||
|
|
||||||
"run.form" run-form
|
|
||||||
"run.val_type" (enc/class-sym run-val)
|
|
||||||
"run.val" run-val
|
|
||||||
"run.nsecs" run-nsecs
|
|
||||||
"sample" sample-rate
|
|
||||||
|
|
||||||
"parent.id" (get parent :id)
|
|
||||||
"parent.uid" (get parent :uid)}))
|
|
||||||
|
|
||||||
attrs-map
|
|
||||||
(enc/if-not [{:keys [type msg data trace]} (enc/ex-map (get signal :error))]
|
|
||||||
attrs-map
|
|
||||||
(merge-prefix-map
|
|
||||||
(enc/assoc-some attrs-map
|
|
||||||
;; 3x standard keys
|
|
||||||
"exception.type" type
|
|
||||||
"exception.message" msg
|
|
||||||
"exception.stacktrace" (when trace (#'utils/format-clj-stacktrace trace)))
|
|
||||||
"exception.data" data))
|
|
||||||
|
|
||||||
kvs (get signal :kvs)
|
|
||||||
attr-kvs
|
|
||||||
(when attrs-key
|
|
||||||
(when-let [kvs (get signal attrs-key)]
|
|
||||||
(not-empty kvs)))
|
|
||||||
|
|
||||||
kvs
|
|
||||||
(if attr-kvs
|
|
||||||
(dissoc kvs attrs-key)
|
|
||||||
(do kvs))
|
|
||||||
|
|
||||||
attrs-map
|
|
||||||
(-> attrs-map
|
|
||||||
(merge-prefix-map "ctx" (get signal :ctx))
|
|
||||||
(merge-prefix-map "data" (get signal :data))
|
|
||||||
(merge-prefix-map "kvs" (get signal :kvs))
|
|
||||||
(enc/fast-merge attr-kvs) ; Unprefixed, undocumented
|
|
||||||
)]
|
|
||||||
|
|
||||||
attrs-map))
|
|
||||||
|
|
||||||
(defn get-default-logger-provider
|
|
||||||
"Experimental, subject to change!! Feedback very welcome!
|
|
||||||
Returns `io.opentelemetry.api.logs.LoggerProvider` via:
|
|
||||||
`AutoConfiguredOpenTelemetrySdk` when possible, or
|
|
||||||
`GlobalOpenTelemetry` otherwise."
|
|
||||||
^LoggerProvider []
|
|
||||||
(or
|
|
||||||
(enc/compile-when
|
|
||||||
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
|
|
||||||
(enc/catching :common
|
|
||||||
(let [builder (io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk/builder)]
|
|
||||||
(.getSdkLoggerProvider (.getOpenTelemetrySdk (.build builder))))))
|
|
||||||
|
|
||||||
(.getLogsBridge (GlobalOpenTelemetry/get))))
|
|
||||||
|
|
||||||
;;;; Handler
|
|
||||||
|
|
||||||
(defn handler:open-telemetry-logger
|
|
||||||
"Experimental, subject to change!! Feedback very welcome!
|
|
||||||
|
|
||||||
Returns a (fn handler [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Emits signal content to the `io.opentelemetry.api.logs.Logger`
|
|
||||||
returned by given `io.opentelemetry.api.logs.LoggerProvider`."
|
|
||||||
|
|
||||||
([] (handler:open-telemetry-logger nil))
|
|
||||||
([{:keys
|
|
||||||
[^LoggerProvider logger-provider
|
|
||||||
attrs-key ; Advanced, undocumented
|
|
||||||
]
|
|
||||||
|
|
||||||
:or
|
|
||||||
{logger-provider (get-default-logger-provider)
|
|
||||||
attrs-key :open-telemetry-attrs}}]
|
|
||||||
|
|
||||||
(let []
|
|
||||||
(fn a-handler:open-telemetry-logger
|
|
||||||
([]) ; Shut down (no-op)
|
|
||||||
([signal]
|
|
||||||
(let [{:keys [ns inst level msg_]} signal
|
|
||||||
logger (.get logger-provider (or ns "default"))
|
|
||||||
severity (level->severity level)
|
|
||||||
msg (force msg_)
|
|
||||||
attrs-map (signal->attrs-map attrs-key signal)
|
|
||||||
attrs (as-attrs attrs-map)]
|
|
||||||
|
|
||||||
(.emit
|
|
||||||
(doto (.logRecordBuilder logger)
|
|
||||||
(.setTimestamp inst)
|
|
||||||
(.setSeverity severity)
|
|
||||||
(.setBody msg)
|
|
||||||
(.setAllAttributes attrs)))))))))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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/shut-down-handlers!`."
|
|
||||||
[] (tel/shut-down-handlers!))
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
(ns taoensso.telemere.tools-logging
|
|
||||||
"Intake support for `clojure.tools.logging` -> Telemere.
|
|
||||||
Telemere will attempt to load this ns automatically when possible."
|
|
||||||
(:require
|
|
||||||
[taoensso.encore :as enc :refer [have have?]]
|
|
||||||
[taoensso.telemere.impl :as impl]
|
|
||||||
[clojure.tools.logging :as ctl]))
|
|
||||||
|
|
||||||
(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))
|
|
||||||
|
|
||||||
(deftype TelemereLogger [logger-ns]
|
|
||||||
|
|
||||||
clojure.tools.logging.impl/Logger
|
|
||||||
(enabled? [_ level]
|
|
||||||
(when-debug (println [:tools.logger/enabled? logger-ns level]))
|
|
||||||
(impl/signal-allowed?
|
|
||||||
{:location nil
|
|
||||||
:kind :log
|
|
||||||
:id :taoensso.telemere/tools-logging
|
|
||||||
:level level}))
|
|
||||||
|
|
||||||
(write! [_ level throwable message]
|
|
||||||
(when-debug (println [:tools.logger/write! logger-ns level]))
|
|
||||||
(impl/signal!
|
|
||||||
{:allow? true ; Pre-filtered by `enabled?` call
|
|
||||||
:location nil
|
|
||||||
:kind :log
|
|
||||||
:id :taoensso.telemere/tools-logging
|
|
||||||
:level level
|
|
||||||
:error throwable
|
|
||||||
:msg message})
|
|
||||||
nil))
|
|
||||||
|
|
||||||
(deftype TelemereLoggerFactory []
|
|
||||||
clojure.tools.logging.impl/LoggerFactory
|
|
||||||
(name [_ ] "taoensso.telemere")
|
|
||||||
(get-logger [_ logger-ns] (TelemereLogger. (str logger-ns))))
|
|
||||||
|
|
||||||
(defn tools-logging->telemere!
|
|
||||||
"Configures `clojure.tools.logging` to use Telemere as its logging implementation.
|
|
||||||
|
|
||||||
Called automatically if the following is true:
|
|
||||||
(get-env {:as :bool} :clojure.tools.logging/to-telemere)
|
|
||||||
|
|
||||||
See `get-env` for details."
|
|
||||||
[]
|
|
||||||
(impl/signal!
|
|
||||||
{:kind :event
|
|
||||||
:level :info
|
|
||||||
:id :taoensso.telemere/clojure.tools.logging->telemere!
|
|
||||||
:msg "Enabling intake: `clojure.tools.logging` -> Telemere"})
|
|
||||||
|
|
||||||
(alter-var-root #'clojure.tools.logging/*logger-factory*
|
|
||||||
(fn [_] (TelemereLoggerFactory.))))
|
|
||||||
|
|
||||||
(defn tools-logging->telemere?
|
|
||||||
"Returns true iff `clojure.tools.logging` is configured to use Telemere
|
|
||||||
as its logging implementation."
|
|
||||||
[]
|
|
||||||
(when-let [lf clojure.tools.logging/*logger-factory*]
|
|
||||||
(instance? TelemereLoggerFactory lf)))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defn check-intake
|
|
||||||
"Returns {:keys [present? sending->telemere? telemere-receiving?]}."
|
|
||||||
[]
|
|
||||||
(let [sending? (tools-logging->telemere?)
|
|
||||||
receiving?
|
|
||||||
(and sending?
|
|
||||||
(impl/test-intake! "`clojure.tools.logging` -> Telemere"
|
|
||||||
#(clojure.tools.logging/info %)))]
|
|
||||||
|
|
||||||
{:present? true
|
|
||||||
:sending->telemere? sending?
|
|
||||||
:telemere-receiving? receiving?}))
|
|
||||||
|
|
||||||
(impl/add-intake-check! :tools-logging check-intake)
|
|
||||||
|
|
||||||
(impl/on-init
|
|
||||||
(when (enc/get-env {:as :bool} :clojure.tools.logging/to-telemere)
|
|
||||||
(tools-logging->telemere!)))
|
|
||||||
|
|
@ -1,470 +0,0 @@
|
||||||
(ns taoensso.telemere.utils
|
|
||||||
"Misc utils useful for Telemere handlers, middleware, etc."
|
|
||||||
(:refer-clojure :exclude [newline])
|
|
||||||
(:require
|
|
||||||
[clojure.string :as str]
|
|
||||||
#?(:clj [clojure.java.io :as jio])
|
|
||||||
[taoensso.encore :as enc :refer [have have?]]))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(require '[taoensso.telemere :as tel])
|
|
||||||
(remove-ns 'taoensso.telemere.utils)
|
|
||||||
(:api (enc/interns-overview)))
|
|
||||||
|
|
||||||
;;;; Private
|
|
||||||
|
|
||||||
(enc/def* ^:no-doc upper-qn
|
|
||||||
"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))
|
|
||||||
|
|
||||||
;;;; Public misc
|
|
||||||
|
|
||||||
(enc/defaliases enc/newline enc/pr-edn #?(:cljs enc/pr-json))
|
|
||||||
|
|
||||||
#?(:clj (defn thread-name "Returns string name of current thread." ^String [] (.getName (Thread/currentThread))))
|
|
||||||
#?(:clj (defn thread-id "Returns long id of current thread." ^long [] (.getId (Thread/currentThread))))
|
|
||||||
|
|
||||||
(comment [(thread-name) (thread-id)])
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn host-ip
|
|
||||||
"Returns cached local host IP address string, or `timeout-val` (default \"UnknownHost\")."
|
|
||||||
( [timeout-msecs timeout-val] (enc/get-host-ip (enc/msecs :mins 1) timeout-msecs timeout-val))
|
|
||||||
(^String [ ] (enc/get-host-ip (enc/msecs :mins 1) 5000 "UnknownHost"))))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn hostname
|
|
||||||
"Returns cached local hostname string, or `timeout-val` (default \"UnknownHost\")."
|
|
||||||
( [timeout-msecs timeout-val] (enc/get-hostname (enc/msecs :mins 1) timeout-msecs timeout-val))
|
|
||||||
(^String [ ] (enc/get-hostname (enc/msecs :mins 1) 3500 (delay (host-ip 1500 "UnknownHost"))))))
|
|
||||||
|
|
||||||
(comment (enc/qb 1e6 (hostname))) ; 56.88
|
|
||||||
|
|
||||||
#?(:cljs
|
|
||||||
(defn js-console-logger
|
|
||||||
"Returns JavaScript console logger to match given signal level:
|
|
||||||
`:trace` -> `js/console.trace`,
|
|
||||||
`:error` -> `js/console.error`, etc.
|
|
||||||
|
|
||||||
Defaults to `js.console.log` for unmatched signal levels.
|
|
||||||
NB: assumes that `js/console` exists, handler constructors should check first!"
|
|
||||||
[level]
|
|
||||||
(case level
|
|
||||||
:trace js/console.trace
|
|
||||||
:debug js/console.debug
|
|
||||||
:info js/console.info
|
|
||||||
:warn js/console.warn
|
|
||||||
:error js/console.error
|
|
||||||
:fatal js/console.error
|
|
||||||
:report js/console.info
|
|
||||||
(do js/console.log))))
|
|
||||||
|
|
||||||
(comment (js-console-logger))
|
|
||||||
|
|
||||||
(defn error-signal?
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns true iff given signal has an `:error` value, or a `:kind` or `:level`
|
|
||||||
that indicates that it's an error."
|
|
||||||
#?(:cljs {:tag 'boolean})
|
|
||||||
[signal]
|
|
||||||
(and signal
|
|
||||||
(boolean
|
|
||||||
(or
|
|
||||||
(get signal :error)
|
|
||||||
(enc/identical-kw? (get signal :kind) :error)
|
|
||||||
(case (get signal :level) (:error :fatal) true false)
|
|
||||||
(get signal :error?) ; User kv
|
|
||||||
))))
|
|
||||||
|
|
||||||
(comment (error-signal? {:level :fatal}))
|
|
||||||
|
|
||||||
(defn error-in-signal->maps
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns given signal with possible `:error` replaced by
|
|
||||||
[{:keys [type msg data]} ...] cause chain.
|
|
||||||
|
|
||||||
Useful when serializing signals to edn/JSON/etc."
|
|
||||||
[signal]
|
|
||||||
(enc/if-let [error (get signal :error)
|
|
||||||
chain (enc/ex-chain :as-map error)]
|
|
||||||
(assoc signal :error chain)
|
|
||||||
(do signal)))
|
|
||||||
|
|
||||||
(comment (error-in-signal->maps {:level :info :error (ex-info "Ex" {})}))
|
|
||||||
|
|
||||||
(defn minify-signal
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns minimal signal map, removing:
|
|
||||||
- Keys with nil values, and
|
|
||||||
- Keys with redundant values (`:kvs`, `:location`, `:file`).
|
|
||||||
|
|
||||||
Useful when serializing signals to edn/JSON/etc."
|
|
||||||
[signal]
|
|
||||||
(reduce-kv
|
|
||||||
(fn [m k v]
|
|
||||||
(if (nil? v)
|
|
||||||
m
|
|
||||||
(case k
|
|
||||||
(:kvs :location :file) m
|
|
||||||
(assoc m k v))))
|
|
||||||
nil signal))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(minify-signal (tel/with-signal (tel/event! ::ev-id1)))
|
|
||||||
(let [s (tel/with-signal (tel/event! ::ev-id1))]
|
|
||||||
(enc/qb 1e6 ; 683
|
|
||||||
(minify-signal s))))
|
|
||||||
|
|
||||||
;;;; Files
|
|
||||||
|
|
||||||
#?(:clj (defn ^:no-doc as-file ^java.io.File [file] (jio/as-file file)))
|
|
||||||
#?(:clj
|
|
||||||
(defn ^:no-doc writeable-file!
|
|
||||||
"Private, don't use.
|
|
||||||
Returns writable `java.io.File`, or throws."
|
|
||||||
^java.io.File [file]
|
|
||||||
(let [file (as-file file)]
|
|
||||||
(when-not (.exists file)
|
|
||||||
(when-let [parent (.getParentFile file)] (.mkdirs parent))
|
|
||||||
(.createNewFile file))
|
|
||||||
|
|
||||||
(if (.canWrite file)
|
|
||||||
file
|
|
||||||
(throw
|
|
||||||
(ex-info "Unable to prepare writable `java.io.File`"
|
|
||||||
{:path (.getAbsolutePath file)}))))))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn ^:no-doc file-stream
|
|
||||||
"Private, don't use.
|
|
||||||
Returns a new `java.io.FileOutputStream` for given `java.io.File`, etc."
|
|
||||||
^java.io.FileOutputStream [file append?]
|
|
||||||
(java.io.FileOutputStream. (as-file file) (boolean append?))))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn file-writer
|
|
||||||
"Experimental, subject to change!!
|
|
||||||
|
|
||||||
Opens the specified file and returns a stateful fn of 2 arities:
|
|
||||||
[content] => Writes given content to file, or no-ops if closed.
|
|
||||||
[] => Closes the writer.
|
|
||||||
|
|
||||||
Thread safe. Automatically creates file and parent dirs as necessary.
|
|
||||||
Writers MUST ALWAYS be manually closed after use!
|
|
||||||
|
|
||||||
Useful for handlers that write to files, etc."
|
|
||||||
[file append?]
|
|
||||||
(let [file (writeable-file! file)
|
|
||||||
stream_ (volatile! (file-stream file append?))
|
|
||||||
open?_ (enc/latom true)
|
|
||||||
|
|
||||||
close!
|
|
||||||
(fn []
|
|
||||||
(when (compare-and-set! open?_ true false)
|
|
||||||
(when-let [^java.io.FileOutputStream stream (.deref stream_)]
|
|
||||||
(.close stream)
|
|
||||||
(vreset! stream_ nil)
|
|
||||||
true)))
|
|
||||||
|
|
||||||
reset!
|
|
||||||
(fn []
|
|
||||||
(close!)
|
|
||||||
(vreset! stream_ (file-stream file append?))
|
|
||||||
(reset! open?_ true)
|
|
||||||
true)
|
|
||||||
|
|
||||||
write-ba!
|
|
||||||
(fn [^bytes ba-content retrying?]
|
|
||||||
(when-let [^java.io.FileOutputStream stream (.deref stream_)]
|
|
||||||
(.write stream ba-content)
|
|
||||||
(.flush stream)
|
|
||||||
true))
|
|
||||||
|
|
||||||
file-exists!
|
|
||||||
(let [rl (enc/rate-limiter-once-per 250)]
|
|
||||||
(fn []
|
|
||||||
(or (rl) (.exists file)
|
|
||||||
(throw (java.io.IOException. "File doesn't exist")))))
|
|
||||||
|
|
||||||
lock (Object.)]
|
|
||||||
|
|
||||||
(fn file-writer
|
|
||||||
([] (when (open?_) (locking lock (close!))))
|
|
||||||
([content-or-action]
|
|
||||||
(case content-or-action ; Undocumented
|
|
||||||
:writer/open? (open?_)
|
|
||||||
:writer/file file
|
|
||||||
:writer/stream (.deref stream_)
|
|
||||||
:writer/reset! (locking lock (reset!))
|
|
||||||
(when (open?_)
|
|
||||||
(let [content content-or-action
|
|
||||||
ba (.getBytes (str content) java.nio.charset.StandardCharsets/UTF_8)]
|
|
||||||
(locking lock
|
|
||||||
(try
|
|
||||||
(file-exists!)
|
|
||||||
(write-ba! ba false)
|
|
||||||
(catch java.io.IOException _
|
|
||||||
(reset!)
|
|
||||||
(write-ba! ba true))))))))))))
|
|
||||||
|
|
||||||
(comment (def fw1 (file-writer "test.txt" true)) (fw1 "x") (fw1))
|
|
||||||
|
|
||||||
;;;; Formatters
|
|
||||||
|
|
||||||
(defn format-nsecs-fn
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns a (fn format [nanosecs]) that:
|
|
||||||
- Takes a long nanoseconds (e.g. runtime).
|
|
||||||
- Returns a formatted human-readable string like:
|
|
||||||
\"1.00m\", \"4.20s\", \"340ms\", \"822μs\", etc."
|
|
||||||
([] (format-nsecs-fn nil))
|
|
||||||
([{:as _opts}] (fn format-nsecs [nanosecs] (enc/format-nsecs nanosecs))))
|
|
||||||
|
|
||||||
(comment ((format-nsecs-fn) 4747463567))
|
|
||||||
|
|
||||||
(enc/defalias enc/format-inst-fn)
|
|
||||||
|
|
||||||
(comment ((format-inst-fn) (enc/now-inst)))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defn- format-clj-stacktrace
|
|
||||||
[trace]
|
|
||||||
(let [sb (enc/str-builder)
|
|
||||||
s+nl (enc/sb-appender sb enc/newline)]
|
|
||||||
(doseq [st-el (force trace)]
|
|
||||||
(let [{:keys [class method file line]} st-el]
|
|
||||||
(s+nl class "/" method " at " file ":" line)))
|
|
||||||
(str sb))))
|
|
||||||
|
|
||||||
(comment (println (format-clj-stacktrace (:trace (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})))))))
|
|
||||||
|
|
||||||
(defn format-error-fn
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns a (fn format [error]) that:
|
|
||||||
- Takes a platform error (`Throwable` or `js/Error`).
|
|
||||||
- Returns a formatted human-readable string"
|
|
||||||
([] (format-error-fn nil))
|
|
||||||
([{:as _opts}]
|
|
||||||
(let [nl enc/newline
|
|
||||||
nls enc/newlines]
|
|
||||||
|
|
||||||
(fn format-error [error]
|
|
||||||
(when-let [em (enc/ex-map error)]
|
|
||||||
(let [sb (enc/str-builder)
|
|
||||||
s+ (partial enc/sb-append sb)
|
|
||||||
{:keys [chain trace]} em]
|
|
||||||
|
|
||||||
(let [s+cause (enc/sb-appender sb (str nls "Caused: "))]
|
|
||||||
(s+ " Root: ")
|
|
||||||
(doseq [{:keys [type msg data]} (rseq chain)]
|
|
||||||
(s+cause type " - " msg)
|
|
||||||
(when data
|
|
||||||
(s+ nl " data: " (enc/pr-edn* data)))))
|
|
||||||
|
|
||||||
(when trace
|
|
||||||
(s+ nl nl "Root stack trace:" nl)
|
|
||||||
#?(:cljs (s+ trace)
|
|
||||||
:clj (format-clj-stacktrace trace)))
|
|
||||||
|
|
||||||
(str sb)))))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(do (throw (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))
|
|
||||||
(do (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))
|
|
||||||
(println (str "--\n" ((format-error-fn) (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))))
|
|
||||||
|
|
||||||
(defn format-signal-prelude-fn
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns a (fn format [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Returns a formatted prelude string like:
|
|
||||||
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere(2,21) ::ev-id - msg\""
|
|
||||||
([] (format-signal-prelude-fn nil))
|
|
||||||
([{:keys [format-inst-fn]
|
|
||||||
:or {format-inst-fn (format-inst-fn)}}]
|
|
||||||
|
|
||||||
(fn format-signal-prelude [signal]
|
|
||||||
(let [{:keys [inst level kind ns id msg_]} signal
|
|
||||||
sb (enc/str-builder)
|
|
||||||
s+spc (enc/sb-appender sb " ")]
|
|
||||||
|
|
||||||
(when inst (when-let [ff format-inst-fn] (s+spc (ff inst))))
|
|
||||||
(when level (s+spc (format-level level)))
|
|
||||||
|
|
||||||
(if kind (s+spc (upper-qn kind)) (s+spc "DEFAULT"))
|
|
||||||
#?(:clj (s+spc (hostname)))
|
|
||||||
|
|
||||||
;; "<ns>:(<line>,<column>)"
|
|
||||||
(when-let [base (or ns (get signal :file))]
|
|
||||||
(let [s+ (partial enc/sb-append sb)] ; Without separator
|
|
||||||
(s+ " " base)
|
|
||||||
(when-let [l (get signal :line)]
|
|
||||||
(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))
|
|
||||||
(str sb)))))
|
|
||||||
|
|
||||||
(comment ((format-signal-prelude-fn) (tel/with-signal (tel/event! ::ev-id))))
|
|
||||||
|
|
||||||
(defn ^:no-doc signal-content-handler
|
|
||||||
"Private, don't use.
|
|
||||||
Returns a (fn handle [signal handle-fn value-fn]) for internal use.
|
|
||||||
Content equivalent to `format-signal-prelude-fn`."
|
|
||||||
([] (signal-content-handler nil))
|
|
||||||
([{:keys [format-nsecs-fn format-error-fn raw-error?]
|
|
||||||
:or
|
|
||||||
{format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
|
|
||||||
format-error-fn (format-error-fn) ; (fn [error])
|
|
||||||
}}]
|
|
||||||
|
|
||||||
(let [err-start (str newline "<<< error <<<" newline)
|
|
||||||
err-stop (str newline ">>> error >>>")]
|
|
||||||
|
|
||||||
(fn a-signal-content-handler [signal hf vf]
|
|
||||||
(let [{:keys [uid parent data kvs ctx sample-rate]} signal]
|
|
||||||
(when sample-rate (hf "sample: " (vf sample-rate)))
|
|
||||||
(when uid (hf " uid: " (vf uid)))
|
|
||||||
(when parent (hf "parent: " (vf parent)))
|
|
||||||
(when data (hf " data: " (vf data)))
|
|
||||||
(when kvs (hf " kvs: " (vf kvs)))
|
|
||||||
(when ctx (hf " ctx: " (vf ctx))))
|
|
||||||
|
|
||||||
(let [{:keys [run-form error]} signal]
|
|
||||||
(when run-form
|
|
||||||
(let [{:keys [run-val run-nsecs]} signal
|
|
||||||
run-time (when run-nsecs (when-let [ff format-nsecs-fn] (ff run-nsecs)))
|
|
||||||
run-info
|
|
||||||
(if error
|
|
||||||
{:form run-form
|
|
||||||
:time run-time
|
|
||||||
:nsecs run-nsecs}
|
|
||||||
|
|
||||||
{:form run-form
|
|
||||||
:time run-time
|
|
||||||
:nsecs run-nsecs
|
|
||||||
:val run-val
|
|
||||||
#?@(:clj [:val-type (enc/class-sym run-val)])})]
|
|
||||||
|
|
||||||
(hf " run: " (vf run-info))))
|
|
||||||
|
|
||||||
(when error
|
|
||||||
(if raw-error?
|
|
||||||
(hf " error: " error)
|
|
||||||
(when-let [ff format-error-fn]
|
|
||||||
(hf err-start (ff error) err-stop)))))))))
|
|
||||||
|
|
||||||
;;;; Signal formatters
|
|
||||||
|
|
||||||
(defn format-signal->edn-fn
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns a (fn format->edn [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Returns edn string of the (minified) signal."
|
|
||||||
([] (format-signal->edn-fn nil))
|
|
||||||
([{:keys [pr-edn-fn prep-fn]
|
|
||||||
:or
|
|
||||||
{pr-edn-fn pr-edn
|
|
||||||
prep-fn (comp error-in-signal->maps minify-signal)}}]
|
|
||||||
|
|
||||||
(fn format-signal->edn [signal]
|
|
||||||
(let [signal* (if prep-fn (prep-fn signal) signal)]
|
|
||||||
(pr-edn-fn signal*)))))
|
|
||||||
|
|
||||||
(comment ((format-signal->edn-fn) {:level :info, :msg "msg"}))
|
|
||||||
|
|
||||||
(defn format-signal->json-fn
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns a (fn format->json [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Returns JSON string of the (minified) signal.
|
|
||||||
|
|
||||||
(Clj only): An appropriate `:pr-json-fn` MUST be provided."
|
|
||||||
([] (format-signal->json-fn nil))
|
|
||||||
([{:keys [pr-json-fn prep-fn]
|
|
||||||
:or
|
|
||||||
{#?@(:cljs [pr-json-fn pr-json])
|
|
||||||
prep-fn (comp error-in-signal->maps minify-signal)}}]
|
|
||||||
|
|
||||||
(when-not pr-json-fn
|
|
||||||
(throw
|
|
||||||
(ex-info (str "No `" `format-signal->json-fn "` `:pr-json-fn` was provided") {})))
|
|
||||||
|
|
||||||
(fn format-signal->json [signal]
|
|
||||||
(let [signal* (if prep-fn (prep-fn signal) signal)]
|
|
||||||
(pr-json-fn signal*)))))
|
|
||||||
|
|
||||||
(comment ((format-signal->json-fn) {:level :info, :msg "msg"}))
|
|
||||||
|
|
||||||
(defn format-signal->str-fn
|
|
||||||
"Experimental, subject to change.
|
|
||||||
Returns a (fn format->str [signal]) that:
|
|
||||||
- Takes a Telemere signal.
|
|
||||||
- Returns a formatted string intended for text consoles, etc."
|
|
||||||
([] (format-signal->str-fn nil))
|
|
||||||
([{:keys [format-signal-prelude-fn
|
|
||||||
format-nsecs-fn format-error-fn]
|
|
||||||
:or
|
|
||||||
{format-signal-prelude-fn (format-signal-prelude-fn) ; (fn [signal])
|
|
||||||
format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
|
|
||||||
format-error-fn (format-error-fn) ; (fn [error])
|
|
||||||
}}]
|
|
||||||
|
|
||||||
(let [signal-content-handler ; (fn [signal hf vf]
|
|
||||||
(signal-content-handler
|
|
||||||
{:format-nsecs-fn format-nsecs-fn
|
|
||||||
:format-error-fn format-error-fn})]
|
|
||||||
|
|
||||||
(fn format-signal->str [signal]
|
|
||||||
(let [sb (enc/str-builder)
|
|
||||||
s+ (partial enc/sb-append sb)
|
|
||||||
s++ (partial enc/sb-append sb (str newline " "))]
|
|
||||||
|
|
||||||
(when-let [ff format-signal-prelude-fn] (s+ (ff signal))) ; Prelude
|
|
||||||
(signal-content-handler signal s++ enc/pr-edn*) ; Content
|
|
||||||
(str sb))))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(tel/with-ctx {:c :C}
|
|
||||||
(println
|
|
||||||
((format-signal->str-fn)
|
|
||||||
(tel/with-signal
|
|
||||||
(tel/event! ::ev-id
|
|
||||||
{:user-k1 #{:a :b :c}
|
|
||||||
:msg "hi"
|
|
||||||
:data {:a :A}
|
|
||||||
;; :error (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))
|
|
||||||
:run (/ 1 0)}))))))
|
|
||||||
|
|
@ -1,907 +0,0 @@
|
||||||
(ns taoensso.telemere-tests
|
|
||||||
(:require
|
|
||||||
[clojure.test :as test :refer [deftest testing is]]
|
|
||||||
[taoensso.encore :as enc :refer [throws? submap?] :rename {submap? sm?}]
|
|
||||||
[taoensso.encore.signals :as sigs]
|
|
||||||
[taoensso.telemere :as tel]
|
|
||||||
[taoensso.telemere.impl :as impl
|
|
||||||
:refer [signal! with-signal with-signals]
|
|
||||||
:rename {signal! sig!, with-signal with-sig, with-signals with-sigs}]
|
|
||||||
|
|
||||||
[taoensso.telemere.utils :as utils]
|
|
||||||
[taoensso.telemere.timbre :as timbre]
|
|
||||||
#_[taoensso.telemere.tools-logging :as tools-logging]
|
|
||||||
#_[taoensso.telemere.streams :as streams]
|
|
||||||
#?(:clj [taoensso.telemere.slf4j :as slf4j])
|
|
||||||
#?(:clj [taoensso.telemere.open-telemetry :as otel])
|
|
||||||
#?(:clj [taoensso.telemere.file-handler :as fh])
|
|
||||||
#?(:clj [clojure.tools.logging :as ctl])))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(remove-ns 'taoensso.telemere-tests)
|
|
||||||
(test/run-tests 'taoensso.telemere-tests))
|
|
||||||
|
|
||||||
;;;; Utils
|
|
||||||
|
|
||||||
(do
|
|
||||||
(def ^:dynamic *dynamic-var* nil)
|
|
||||||
|
|
||||||
(def t0s "2024-06-09T21:15:20.170Z")
|
|
||||||
(def t0 (enc/as-inst t0s))
|
|
||||||
(def udt0 (enc/as-udt t0))
|
|
||||||
|
|
||||||
(def ex-info-type (#'enc/ex-type (ex-info "" {})))
|
|
||||||
(def ex1 (ex-info "Ex1" {}))
|
|
||||||
(def ex2 (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"})))
|
|
||||||
(defn ex1! [] (throw ex1))
|
|
||||||
|
|
||||||
(defn ex1? [x] (= (enc/ex-root x) ex1))
|
|
||||||
(def pex1? (enc/pred ex1?))
|
|
||||||
(def pstr? (enc/pred string?))
|
|
||||||
(def pnat-int? (enc/pred enc/nat-int?))
|
|
||||||
(def pinst? (enc/pred enc/inst?)))
|
|
||||||
|
|
||||||
(let [rt-sig-filter_ (atom nil)
|
|
||||||
sig-handlers_ (atom nil)]
|
|
||||||
|
|
||||||
(test/use-fixtures :once
|
|
||||||
(enc/test-fixtures
|
|
||||||
{:before
|
|
||||||
(fn []
|
|
||||||
(reset! rt-sig-filter_ impl/*rt-sig-filter*)
|
|
||||||
(reset! sig-handlers_ impl/*sig-handlers*)
|
|
||||||
(enc/set-var-root! impl/*sig-handlers* nil)
|
|
||||||
(enc/set-var-root! impl/*rt-sig-filter* nil))
|
|
||||||
|
|
||||||
:after
|
|
||||||
(fn []
|
|
||||||
(enc/set-var-root! impl/*rt-sig-filter* @rt-sig-filter_)
|
|
||||||
(enc/set-var-root! impl/*sig-handlers* @sig-handlers_))})))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(deftest _parse-msg-form
|
|
||||||
(let [pmf @#'impl/parse-msg-form]
|
|
||||||
[(is (= (pmf '["foo"])) '"foo")
|
|
||||||
(is (= (pmf '["foo" "bar"]) '(clojure.core/delay (taoensso.telemere.impl/signal-msg ["foo" "bar"]))))
|
|
||||||
(is (= (pmf 'my-symbol) 'my-symbol))])))
|
|
||||||
|
|
||||||
(deftest _impl-misc
|
|
||||||
;; Note lots of low-level signal/filtering tests in `taoensso.encore`
|
|
||||||
[(is (= (impl/signal-msg
|
|
||||||
["x" "y" nil ["z1" nil "z2" "z3"]
|
|
||||||
(impl/msg-splice ["s1" nil "s2" "s3" impl/msg-skip "s4"])
|
|
||||||
(impl/msg-splice nil)
|
|
||||||
impl/msg-skip :kw])
|
|
||||||
|
|
||||||
"x y nil [\"z1\" nil \"z2\" \"z3\"] s1 nil s2 s3 s4 :kw"))])
|
|
||||||
|
|
||||||
(deftest _signal-macro
|
|
||||||
[(is (= (with-sigs (sig! {:level :info, :elide? true })) [[nil nil] nil]) "With compile-time elision")
|
|
||||||
(is (= (with-sigs (sig! {:level :info, :elide? true, :run (+ 1 2)})) [[3 nil] nil]) "With compile-time elision, run-form")
|
|
||||||
(is (= (with-sigs (sig! {:level :info, :allow? false })) [[nil nil] nil]) "With runtime suppression")
|
|
||||||
(is (= (with-sigs (sig! {:level :info, :allow? false, :run (+ 1 2)})) [[3 nil] nil]) "With runtime suppression, run-form")
|
|
||||||
|
|
||||||
(is (->> (sig! {:level :info, :elide? true, :run (ex1!)}) (throws? :ex-info "Ex1")) "With compile-time elision, throwing run-form")
|
|
||||||
(is (->> (sig! {:level :info, :allow? false, :run (ex1!)}) (throws? :ex-info "Ex1")) "With runtime suppression, throwing run-form")
|
|
||||||
|
|
||||||
(let [[[rv1 _] [sv1]] (with-sigs (sig! {:level :info }))
|
|
||||||
[[rv2 -] [sv2]] (with-sigs (sig! {:level :info, :run (+ 1 2)}))]
|
|
||||||
|
|
||||||
[(is (= rv1 true)) (is (sm? sv1 {:ns "taoensso.telemere-tests", :level :info, :run-form nil, :run-val nil, :run-nsecs nil}))
|
|
||||||
(is (= rv2 3)) (is (sm? sv2 {:ns "taoensso.telemere-tests", :level :info, :run-form '(+ 1 2), :run-val 3, :run-nsecs pnat-int?}))])
|
|
||||||
|
|
||||||
(testing "Nested signals"
|
|
||||||
(let [[[outer-rv _] [outer-sv]] (with-sigs (sig! {:level :info, :run (with-sigs (sig! {:level :warn, :run "inner-run"}))}))
|
|
||||||
[[inner-rv _] [inner-sv]] outer-rv]
|
|
||||||
|
|
||||||
[(is (= inner-rv "inner-run"))
|
|
||||||
(is (sm? inner-sv {:level :warn, :run-val "inner-run"}))
|
|
||||||
(is (sm? outer-sv {:level :info :run-val [[inner-rv nil] [inner-sv]]}))]))
|
|
||||||
|
|
||||||
(testing "Instants"
|
|
||||||
(let [sv1 (with-sig (sig! {:level :info }))
|
|
||||||
sv2 (with-sig (sig! {:level :info, :run (reduce + (range 1e6))}))
|
|
||||||
sv3 (with-sig (sig! {:level :info, :run (reduce + (range 1e6))
|
|
||||||
:inst ; Allow custom instant
|
|
||||||
#?(:clj java.time.Instant/EPOCH
|
|
||||||
:cljs (js/Date. 0))}))]
|
|
||||||
|
|
||||||
[(let [{start :inst, end :end-inst} sv1]
|
|
||||||
[(is (enc/inst? start))
|
|
||||||
(is (nil? end))])
|
|
||||||
|
|
||||||
(let [{start :inst, end :end-inst} sv2]
|
|
||||||
[(is (enc/inst? start))
|
|
||||||
(is (enc/inst? end))
|
|
||||||
(is (> (inst-ms end) (inst-ms start)))])
|
|
||||||
|
|
||||||
(let [{start :inst, end :end-inst} sv3]
|
|
||||||
[(is (enc/inst? start))
|
|
||||||
(is (enc/inst? end))
|
|
||||||
(is (= (inst-ms start) 0) "Respect custom instant")
|
|
||||||
(is (> (inst-ms end) (inst-ms start)) "End instant is start + run-nsecs")
|
|
||||||
(is (< (inst-ms end) 1e6) "End instant is start + run-nsecs")])]))
|
|
||||||
|
|
||||||
(testing "Support arb extra user kvs"
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :my-k1 "v1", :my-k2 "v2"}))]
|
|
||||||
(is (sm? sv {:level :info, :my-k1 "v1", :my-k2 "v2"
|
|
||||||
:kvs {:my-k1 "v1", :my-k2 "v2"}}))))
|
|
||||||
|
|
||||||
(testing "`:msg` basics"
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :msg "msg1"})) ; No delay
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :msg [ "msg2:" (c)]})) ; Auto delay
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :msg (delay (str "msg3: " (c)))})) ; Manual delay
|
|
||||||
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :run (c), :msg (str "msg4: " (c))})) ; No delay
|
|
||||||
[[rv5 _] [sv5]] (with-sigs :raw nil (sig! {:level :info, :run (c), :msg (str "msg5: " (c)), :allow? false}))]
|
|
||||||
|
|
||||||
[(is (= rv1 0)) (is (= (:msg_ sv1) "msg1"))
|
|
||||||
(is (= rv2 1)) (is (= @(:msg_ sv2) "msg2: 6"))
|
|
||||||
(is (= rv3 2)) (is (= @(:msg_ sv3) "msg3: 7"))
|
|
||||||
(is (= rv4 3)) (is (= (:msg_ sv4) "msg4: 4"))
|
|
||||||
(is (= rv5 5)) (is (= (:msg_ sv5) nil))
|
|
||||||
(is (= @c 8) "5x run + 3x message (1x suppressed)")]))
|
|
||||||
|
|
||||||
(testing "`:data` basics"
|
|
||||||
(vec
|
|
||||||
(for [dk [:data :my-k1]] ; User kvs share same behaviour as data
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), dk {:c1 (c)}}))
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), dk (delay {:c2 (c)})}))
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), dk {:c3 (c)}, :allow? false}))
|
|
||||||
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :run (c), dk (delay {:c4 (c)}), :allow? false}))
|
|
||||||
[[rv5 _] [sv5]] (with-sigs :raw nil (sig! {:level :info, :run (c), dk [:c5 (c)]}))
|
|
||||||
[[rv6 _] [sv6]] (with-sigs :raw nil (sig! {:level :info, :run (c), dk (delay [:c6 (c)])}))]
|
|
||||||
|
|
||||||
[(is (= rv1 0)) (is (= (get sv1 dk) {:c1 1}))
|
|
||||||
(is (= rv2 2)) (is (= (force (get sv2 dk)) {:c2 8}))
|
|
||||||
(is (= rv3 3)) (is (= (get sv3 dk) nil))
|
|
||||||
(is (= rv4 4)) (is (= (force (get sv4 dk)) nil))
|
|
||||||
(is (= rv5 5)) (is (= (get sv5 dk) [:c5 6]) "`:data` can be any type")
|
|
||||||
(is (= rv6 7)) (is (= (force (get sv6 dk)) [:c6 9]) "`:data` can be any type")
|
|
||||||
(is (= @c 10) "6x run + 4x data (2x suppressed)")]))))
|
|
||||||
|
|
||||||
(testing "`:let` basics"
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [_ (c)]}))
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [_ (c)], :allow? false}))
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [_ (c)]}))]
|
|
||||||
|
|
||||||
[(is (= rv1 0))
|
|
||||||
(is (= rv2 2))
|
|
||||||
(is (= rv3 3))
|
|
||||||
(is (= @c 5) "3x run + 2x let (1x suppressed)")]))
|
|
||||||
|
|
||||||
(testing "`:let` + `:msg`"
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [n (c)], :msg "msg1"})) ; No delay
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [n (c)], :msg [ "msg2:" n (c)]})) ; Auto delay
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [n (c)], :msg (delay (str "msg3: " n " " (c)))})) ; Manual delay
|
|
||||||
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [n (c)], :msg (str "msg4: " n " " (c))})) ; No delay
|
|
||||||
[[rv5 _] [sv5]] (with-sigs :raw nil (sig! {:level :info, :run (c), :let [n (c)], :msg (str "msg5: " n " " (c)), :allow? false}))]
|
|
||||||
|
|
||||||
[(is (= rv1 0)) (is (= (:msg_ sv1) "msg1"))
|
|
||||||
(is (= rv2 2)) (is (= @(:msg_ sv2) "msg2: 3 10"))
|
|
||||||
(is (= rv3 4)) (is (= @(:msg_ sv3) "msg3: 5 11"))
|
|
||||||
(is (= rv4 6)) (is (= (:msg_ sv4) "msg4: 7 8"))
|
|
||||||
(is (= rv5 9)) (is (= (:msg_ sv5) nil))
|
|
||||||
(is (= @c 12) "5x run + 4x let (1x suppressed) + 3x msg (1x suppressed)")]))
|
|
||||||
|
|
||||||
(testing "`:do` + `:let` + `:data`/`:my-k1`"
|
|
||||||
(vec
|
|
||||||
(for [dk [:data :my-k1]]
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :do (c), :let [n (c)], dk {:n n, :c1 (c)}}))
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :do (c), :let [n (c)], dk (delay {:n n, :c2 (c)})}))
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :do (c), :let [n (c)], dk {:n n, :c3 (c)}, :allow? false}))
|
|
||||||
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :run (c), :do (c), :let [n (c)], dk (delay {:n n, :c4 (c)}), :allow? false}))
|
|
||||||
[[rv5 _] [sv5]] (with-sigs :raw nil (sig! {:level :info, :run (c), :do (c), :let [n (c)], dk [:n n, :c5 (c)]}))
|
|
||||||
[[rv6 _] [sv6]] (with-sigs :raw nil (sig! {:level :info, :run (c), :do (c), :let [n (c)], dk (delay [:n n, :c6 (c)])}))]
|
|
||||||
|
|
||||||
[(is (= rv1 0)) (is (= (get sv1 dk) {:n 2, :c1 3}))
|
|
||||||
(is (= rv2 4)) (is (= (force (get sv2 dk)) {:n 6, :c2 16}))
|
|
||||||
(is (= rv3 7)) (is (= (get sv3 dk) nil))
|
|
||||||
(is (= rv4 8)) (is (= (force (get sv4 dk)) nil))
|
|
||||||
(is (= rv5 9)) (is (= (get sv5 dk) [:n 11, :c5 12]))
|
|
||||||
(is (= rv6 13)) (is (= (force (get sv6 dk)) [:n 15, :c6 17]))
|
|
||||||
(is (= @c 18) "6x run + 4x do (2x suppressed) + 4x let (2x suppressed) + 4x data (2x suppressed)")]))))
|
|
||||||
|
|
||||||
(testing "Manual `let` (unconditional) + `:data`/`:my-k1`"
|
|
||||||
(vec
|
|
||||||
(for [dk [:data :my-k1]]
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (let [n (c)] (sig! {:level :info, :run (c), dk {:n n, :c1 (c)}})))
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (let [n (c)] (sig! {:level :info, :run (c), dk (delay {:n n, :c2 (c)})})))
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (let [n (c)] (sig! {:level :info, :run (c), dk {:n n, :c3 (c)}, :allow? false})))
|
|
||||||
[[rv4 _] [sv4]] (with-sigs :raw nil (let [n (c)] (sig! {:level :info, :run (c), dk (delay {:n n, :c4 (c)}), :allow? false})))
|
|
||||||
[[rv5 _] [sv5]] (with-sigs :raw nil (let [n (c)] (sig! {:level :info, :run (c), dk [:n n, :c5 (c)]})))
|
|
||||||
[[rv6 _] [sv6]] (with-sigs :raw nil (let [n (c)] (sig! {:level :info, :run (c), dk (delay [:n n, :c6 (c)])})))]
|
|
||||||
|
|
||||||
[(is (= rv1 1)) (is (= (get sv1 dk) {:n 0, :c1 2}))
|
|
||||||
(is (= rv2 4)) (is (= (force (get sv2 dk)) {:n 3, :c2 14}))
|
|
||||||
(is (= rv3 6)) (is (= (get sv3 dk) nil))
|
|
||||||
(is (= rv4 8)) (is (= (force (get sv4 dk)) nil))
|
|
||||||
(is (= rv5 10)) (is (= (get sv5 dk) [:n 9, :c5 11]))
|
|
||||||
(is (= rv6 13)) (is (= (force (get sv6 dk)) [:n 12, :c6 15]))
|
|
||||||
(is (= @c 16) "6x run + 6x let (0x suppressed) + 4x data (2x suppressed)")]))))
|
|
||||||
|
|
||||||
(testing "Call middleware"
|
|
||||||
(let [c (enc/counter)
|
|
||||||
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}))
|
|
||||||
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false}))
|
|
||||||
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}))
|
|
||||||
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware [(fn [_] "signal-value")]}))]
|
|
||||||
|
|
||||||
[(is (= rv1 0)) (is (sm? sv1 {:m1 1 :m2 2}))
|
|
||||||
(is (= rv2 3)) (is (nil? sv2))
|
|
||||||
(is (= rv3 4)) (is (sm? sv3 {:m1 5 :m2 6}))
|
|
||||||
(is (= rv4 true)) (is (= sv4 "signal-value"))
|
|
||||||
(is (= @c 7) "3x run + 4x middleware")]))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(testing "Printing"
|
|
||||||
(let [sv1 (with-sig (sig! {:level :info, :run (+ 1 2), :my-k1 :my-v1}))
|
|
||||||
sv1 ; Ensure instants are printable
|
|
||||||
(-> sv1
|
|
||||||
(update :inst enc/inst->udt)
|
|
||||||
(update :end-inst enc/inst->udt))]
|
|
||||||
|
|
||||||
[(is (= sv1 (read-string (pr-str sv1))))])))])
|
|
||||||
|
|
||||||
(deftest _handlers
|
|
||||||
;; Basic handler tests are in Encore
|
|
||||||
[(testing "Handler middleware"
|
|
||||||
(let [c (enc/counter)
|
|
||||||
sv-h1_ (atom nil)
|
|
||||||
sv-h2_ (atom nil)
|
|
||||||
wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})
|
|
||||||
wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})]
|
|
||||||
|
|
||||||
;; Note that call middleware output is cached and shared across all handlers
|
|
||||||
(binding [impl/*sig-handlers* [wh1 wh2]]
|
|
||||||
(let [;; 1x run + 4x handler middleware + 2x call middleware = 7x
|
|
||||||
rv1 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})
|
|
||||||
sv1-h1 @sv-h1_
|
|
||||||
sv1-h2 @sv-h2_
|
|
||||||
c1 @c
|
|
||||||
|
|
||||||
;; 1x run
|
|
||||||
rv2 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false})
|
|
||||||
sv2-h1 @sv-h1_
|
|
||||||
sv2-h2 @sv-h2_
|
|
||||||
c2 @c ; 8
|
|
||||||
|
|
||||||
;; 1x run + 4x handler middleware + 2x call middleware = 7x
|
|
||||||
rv3 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})
|
|
||||||
sv3-h1 @sv-h1_
|
|
||||||
sv3-h2 @sv-h2_
|
|
||||||
c3 @c ; 15
|
|
||||||
|
|
||||||
;; 4x handler middleware
|
|
||||||
rv4 (sig! {:level :info, :middleware [(fn [_] {:my-sig-val? true})]})
|
|
||||||
sv4-h1 @sv-h1_
|
|
||||||
sv4-h2 @sv-h2_
|
|
||||||
c4 @c]
|
|
||||||
|
|
||||||
[(is (= rv1 0)) (is (sm? sv1-h1 {:m1 1, :m2 2, :hm1 3, :hm2 4})) (is (sm? sv1-h2 {:m1 1, :m2 2, :hm1 5, :hm2 6}))
|
|
||||||
(is (= rv2 7)) (is (sm? sv2-h1 {:m1 1, :m2 2, :hm1 3, :hm2 4})) (is (sm? sv2-h2 {:m1 1, :m2 2, :hm1 5, :hm2 6}))
|
|
||||||
(is (= rv3 8)) (is (sm? sv3-h1 {:m1 9, :m2 10, :hm1 11, :hm2 12})) (is (sm? sv3-h2 {:m1 9, :m2 10, :hm1 13, :hm2 14}))
|
|
||||||
(is (= rv4 true)) (is (sm? sv4-h1 {:my-sig-val? true, :hm1 15, :hm2 16})) (is (sm? sv4-h2 {:my-sig-val? true, :hm1 17, :hm2 18}))
|
|
||||||
(is (= c1 7) "1x run + 4x handler middleware + 2x call middleware")
|
|
||||||
(is (= c2 8) "2x run + 4x handler middleware + 2x call middleware")
|
|
||||||
(is (= c3 15) "3x run + 8x handler middleware + 4x call middleware")
|
|
||||||
(is (= c4 19) "3x run + 12x handler middleware + 4x call middleware")]))))
|
|
||||||
|
|
||||||
(testing "Handler binding conveyance"
|
|
||||||
(let [a (atom nil)
|
|
||||||
wh1
|
|
||||||
(sigs/wrap-handler :hid1 (fn [x] (reset! a *dynamic-var*))
|
|
||||||
nil #?(:clj {:async {:mode :dropping}} :cljs nil))]
|
|
||||||
|
|
||||||
(binding [*dynamic-var* "bound", impl/*sig-handlers* [wh1]] (sig! {:level :info}))
|
|
||||||
(is (= (do #?(:clj (Thread/sleep 500)) @a) "bound"))))])
|
|
||||||
|
|
||||||
(def ^:dynamic *throwing-handler-middleware?* false)
|
|
||||||
|
|
||||||
(deftest _throwing
|
|
||||||
(let [sv_ (atom :nx)
|
|
||||||
error_ (atom :nx)
|
|
||||||
reset-state!
|
|
||||||
(fn []
|
|
||||||
(reset! sv_ :nx)
|
|
||||||
(reset! error_ :nx)
|
|
||||||
true)]
|
|
||||||
|
|
||||||
(tel/with-handler :hid1
|
|
||||||
(fn [sv] (force (:data sv)) (reset! sv_ sv))
|
|
||||||
{:async nil, :error-fn (fn [x] (reset! error_ x)), :rl-error nil,
|
|
||||||
:middleware [(fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))]}
|
|
||||||
|
|
||||||
[(is (->> (sig! {:level :info, :when (ex1!)}) (throws? :ex-info "Ex1")) "`~filterable-expansion/allow` throws at call")
|
|
||||||
(is (->> (sig! {:level :info, :inst (ex1!)}) (throws? :ex-info "Ex1")) "`~inst-form` throws at call")
|
|
||||||
(is (->> (sig! {:level :info, :id (ex1!)}) (throws? :ex-info "Ex1")) "`~id-form` throws at call")
|
|
||||||
(is (->> (sig! {:level :info, :uid (ex1!)}) (throws? :ex-info "Ex1")) "`~uid-form` throws at call")
|
|
||||||
(is (->> (sig! {:level :info, :run (ex1!)}) (throws? :ex-info "Ex1")) "`~run-form` rethrows at call")
|
|
||||||
(is (sm? @sv_ {:level :info, :error pex1?}) "`~run-form` rethrows at call *after* dispatch")
|
|
||||||
|
|
||||||
(testing "`@signal-value_`: trap with wrapped handler"
|
|
||||||
[(testing "Throwing `~let-form`"
|
|
||||||
(reset-state!)
|
|
||||||
[(is (true? (sig! {:level :info, :let [_ (ex1!)]})))
|
|
||||||
(is (= @sv_ :nx))
|
|
||||||
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))])
|
|
||||||
|
|
||||||
(testing "Throwing call middleware"
|
|
||||||
(reset-state!)
|
|
||||||
[(is (true? (sig! {:level :info, :middleware [(fn [_] (ex1!))]})))
|
|
||||||
(is (= @sv_ :nx))
|
|
||||||
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))])
|
|
||||||
|
|
||||||
(testing "Throwing handler middleware"
|
|
||||||
(reset-state!)
|
|
||||||
(binding [*throwing-handler-middleware?* true]
|
|
||||||
[(is (true? (sig! {:level :info})))
|
|
||||||
(is (= @sv_ :nx))
|
|
||||||
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))]))
|
|
||||||
|
|
||||||
(testing "Throwing `@data_`"
|
|
||||||
(reset-state!)
|
|
||||||
[(is (true? (sig! {:level :info, :data (delay (ex1!))})))
|
|
||||||
(is (= @sv_ :nx))
|
|
||||||
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))])
|
|
||||||
|
|
||||||
(testing "Throwing user kv"
|
|
||||||
(reset-state!)
|
|
||||||
[(is (true? (sig! {:level :info, :my-k1 (ex1!)})))
|
|
||||||
(is (= @sv_ :nx))
|
|
||||||
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))])])])))
|
|
||||||
|
|
||||||
(deftest _ctx
|
|
||||||
(testing "Context (`*ctx*`)"
|
|
||||||
[(is (= (binding [tel/*ctx* "my-ctx"] tel/*ctx*) "my-ctx") "Supports manual `binding`")
|
|
||||||
(is (= (tel/with-ctx "my-ctx" tel/*ctx*) "my-ctx") "Supports any data type")
|
|
||||||
|
|
||||||
(is (= (tel/with-ctx "my-ctx1" (tel/with-ctx+ nil tel/*ctx*)) "my-ctx1") "nil update => keep old-ctx")
|
|
||||||
(is (= (tel/with-ctx "my-ctx1" (tel/with-ctx+ (fn [old] [old "my-ctx2"]) tel/*ctx*)) ["my-ctx1" "my-ctx2"]) "fn update => apply")
|
|
||||||
(is (= (tel/with-ctx {:a :A1 :b :B1} (tel/with-ctx+ {:a :A2 :c :C2} tel/*ctx*)) {:a :A2 :b :B1 :c :C2}) "map update => merge")
|
|
||||||
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :ctx "my-ctx"}))] (is (sm? sv {:ctx "my-ctx"}) "Can be set via call opt"))]))
|
|
||||||
|
|
||||||
(deftest _tracing
|
|
||||||
(testing "Tracing"
|
|
||||||
[(let [sv (with-sig (sig! {:level :info }))] (is (sm? sv {:parent nil})))
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :parent {:id :id0}}))] (is (sm? sv {:parent {:id :id0 :uid :submap/nx}}) "`:parent/id` can be set via call opt"))
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :parent {:uid :uid0}}))] (is (sm? sv {:parent {:id :submap/nx :uid :uid0}}) "`:parent/uid` can be set via call opt"))
|
|
||||||
|
|
||||||
(testing "Auto call id, uid"
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :parent {:id :id0, :uid :uid0}, :run impl/*trace-parent*, :data impl/*trace-parent*}))]
|
|
||||||
[(is (sm? sv {:parent {:id :id0, :uid :uid0}}))
|
|
||||||
(is (sm? sv {:run-val {:id nil, :uid (get sv :uid ::nx)}}) "`*trace-parent*` visible to run-form, bound to call's auto {:keys [id uid]}")
|
|
||||||
(is (sm? sv {:data nil}) "`*trace-parent*` not visible to data-form ")]))
|
|
||||||
|
|
||||||
(testing "Manual call id, uid"
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :parent {:id :id0, :uid :uid0}, :id :id1, :uid :uid1, :run impl/*trace-parent*, :data impl/*trace-parent*}))]
|
|
||||||
[(is (sm? sv {:parent {:id :id0, :uid :uid0}}))
|
|
||||||
(is (sm? sv {:run-val {:id :id1, :uid :uid1}}) "`*trace-parent*` visible to run-form, bound to call's auto {:keys [id uid]}")
|
|
||||||
(is (sm? sv {:data nil}) "`*trace-parent*` not visible to data-form ")]))
|
|
||||||
|
|
||||||
(testing "Tracing can be disabled via call opt"
|
|
||||||
(let [sv (with-sig (sig! {:level :info, :parent {:id :id0, :uid :uid0}, :id :id1, :uid :uid1, :run impl/*trace-parent*, :data impl/*trace-parent*, :trace? false}))]
|
|
||||||
[(is (sm? sv {:parent {:id :id0, :uid :uid0}}))
|
|
||||||
(is (sm? sv {:run-val nil}))]))
|
|
||||||
|
|
||||||
(testing "Signal nesting"
|
|
||||||
(let [[[outer-rv _] [outer-sv]]
|
|
||||||
(with-sigs
|
|
||||||
(sig! { :level :info, :id :id1, :uid :uid1,
|
|
||||||
:run (with-sigs (sig! {:level :info, :id :id2, :uid :uid2, :run impl/*trace-parent*}))}))
|
|
||||||
|
|
||||||
[[inner-rv _] [inner-sv]] outer-rv]
|
|
||||||
|
|
||||||
[(is (sm? outer-sv {:id :id1, :uid :uid1, :parent nil}))
|
|
||||||
(is (sm? inner-rv {:id :id2, :uid :uid2}))
|
|
||||||
(is (sm? inner-sv {:parent {:id :id1, :uid :uid1}}))
|
|
||||||
(is (sm? inner-sv {:run-val {:id :id2, :uid :uid2}}))]))]))
|
|
||||||
|
|
||||||
(deftest _sampling
|
|
||||||
;; Capture combined (call * handler) sample rate in Signal when possible
|
|
||||||
(let [test1
|
|
||||||
(fn [call-sample-rate handler-sample-rate]
|
|
||||||
(let [c (enc/counter)
|
|
||||||
sr_ (atom nil)]
|
|
||||||
(tel/with-handler "h1"
|
|
||||||
(fn h1 [x] (c) (compare-and-set! sr_ nil (:sample-rate x)))
|
|
||||||
{:async nil, :sample-rate handler-sample-rate}
|
|
||||||
(do
|
|
||||||
;; Repeat to ensure >=1 gets through sampling
|
|
||||||
(dotimes [_ 1000] (sig! {:level :info, :sample-rate call-sample-rate}))
|
|
||||||
[@sr_ @c]))))]
|
|
||||||
|
|
||||||
[(is (= (test1 nil nil) [nil 1000]) "[none none] = none")
|
|
||||||
(is (= (test1 nil (fn [] nil)) [nil 1000]) "[none =>none] = none")
|
|
||||||
(is (= (test1 1.0 nil) [1.0 1000]) "[100% none] = 100%")
|
|
||||||
(is (= (test1 1.0 (fn [] nil)) [1.0 1000]) "[100% none] = 100%")
|
|
||||||
(is (= (test1 nil 1.0) [1.0 1000]) "[none 100%] = 100%")
|
|
||||||
(is (= (test1 nil (fn [] 1.0)) [1.0 1000]) "[none =>100%] = 100%")
|
|
||||||
|
|
||||||
(is (= (test1 0.0 nil) [nil 0]) "[0% none] = 0%")
|
|
||||||
(is (= (test1 0.0 (fn [] nil)) [nil 0]) "[0% =>none] = 0%")
|
|
||||||
(is (= (test1 nil 0.0) [nil 0]) "[none 0%] = 0%")
|
|
||||||
(is (= (test1 nil (fn [] 0.0)) [nil 0]) "[none =>0%] = 0%")
|
|
||||||
|
|
||||||
(let [[sr n] (test1 0.5 0.5) ] (is (and (= sr 0.25) (<= 150 n 350)) "[50% 50%] = 25%"))
|
|
||||||
(let [[sr n] (test1 0.5 (fn [] 0.5))] (is (and (= sr 0.25) (<= 150 n 350)) "[50% =>50%] = 25%"))]))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(deftest _common-signals
|
|
||||||
[#?(:clj
|
|
||||||
(testing "signal-opts"
|
|
||||||
[(is (= (impl/signal-opts `foo! {:level :info} :id :level :dsc [::my-id ]) {:level :info, :id ::my-id}))
|
|
||||||
(is (= (impl/signal-opts `foo! {:level :info} :id :level :dsc [::my-id :warn ]) {:level :warn, :id ::my-id}))
|
|
||||||
(is (= (impl/signal-opts `foo! {:level :info} :id :level :dsc [::my-id {:level :warn}]) {:level :warn, :id ::my-id}))
|
|
||||||
|
|
||||||
(is (= (impl/signal-opts `foo! {:level :info} :id :level :asc [ ::my-id]) {:level :info, :id ::my-id}))
|
|
||||||
(is (= (impl/signal-opts `foo! {:level :info} :id :level :asc [:warn ::my-id]) {:level :warn, :id ::my-id}))
|
|
||||||
(is (= (impl/signal-opts `foo! {:level :info} :id :level :asc [{:level :warn} ::my-id]) {:level :warn, :id ::my-id}))
|
|
||||||
|
|
||||||
(is (= (impl/signal-catch-opts {:id :main-id, :location {:ns "ns"}, :catch->error true}) [{:id :main-id, :location {:ns "ns"}} {:location {:ns "ns"}, :id :main-id}]))
|
|
||||||
(is (= (impl/signal-catch-opts {:id :main-id, :location {:ns "ns"}, :catch->error :error-id}) [{:id :main-id, :location {:ns "ns"}} {:location {:ns "ns"}, :id :error-id}]))
|
|
||||||
(is (= (impl/signal-catch-opts {:id :main-id, :location {:ns "ns"}, :catch->error {:id :error-id}}) [{:id :main-id, :location {:ns "ns"}} {:location {:ns "ns"}, :id :error-id}]))
|
|
||||||
|
|
||||||
(is (throws? :ex-info "Invalid `foo!` args: single map arg is USUALLY a mistake" (impl/signal-opts `foo! {:level :info} :id :level :dsc [{:msg "msg"}])))
|
|
||||||
(is (throws? :ex-info "Invalid `foo!` args: given opts should not contain `:id`" (impl/signal-opts `foo! {:level :info} :id :level :dsc [:my-id1 {:id ::my-id2}])))]))
|
|
||||||
|
|
||||||
(testing "event!" ; id + ?level => allowed?
|
|
||||||
[(let [[[rv] [sv]] (with-sigs (tel/event! :id1 ))] [(is (= rv true)) (is (sm? sv {:kind :event, :line :submap/ex, :level :info, :id :id1}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/event! :id1 :warn ))] [(is (= rv true)) (is (sm? sv {:kind :event, :line :submap/ex, :level :warn, :id :id1}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/event! :id1 {:level :warn}))] [(is (= rv true)) (is (sm? sv {:kind :event, :line :submap/ex, :level :warn, :id :id1}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/event! :id1 {:allow? false}))] [(is (= rv nil)) (is (nil? sv))])])
|
|
||||||
|
|
||||||
(testing "error!" ; error + ?id => error
|
|
||||||
[(let [[[rv] [sv]] (with-sigs (tel/error! ex1))] [(is (ex1? rv)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id nil}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/error! :id1 ex1))] [(is (ex1? rv)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id :id1}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/error! {:id :id1} ex1))] [(is (ex1? rv)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id :id1}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/error! {:allow? false} ex1))] [(is (ex1? rv)) (is (nil? sv))])])
|
|
||||||
|
|
||||||
(testing "log!" ; msg + ?level => allowed?
|
|
||||||
[(let [[[rv] [sv]] (with-sigs (tel/log! "msg"))] [(is (= rv true)) (is (sm? sv {:kind :log, :line :submap/ex, :msg_ "msg", :level :info}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/log! :warn "msg"))] [(is (= rv true)) (is (sm? sv {:kind :log, :line :submap/ex, :msg_ "msg", :level :warn}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/log! {:level :warn} "msg"))] [(is (= rv true)) (is (sm? sv {:kind :log, :line :submap/ex, :msg_ "msg", :level :warn}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/log! {:allow? false} "msg"))] [(is (= rv nil)) (is (nil? sv))])])
|
|
||||||
|
|
||||||
(testing "catch->error!" ; form + ?id => run value or ?return
|
|
||||||
[(let [[[rv re] [sv]] (with-sigs (tel/catch->error! (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! (ex1!)))] [(is (ex1? re)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id nil}))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! :id1 (ex1!)))] [(is (ex1? re)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id :id1}))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! {:id :id1} (ex1!)))] [(is (ex1? re)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id :id1}))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! {:rethrow? false} (ex1!)))] [(is (nil? re)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id nil}))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! {:catch-val :foo} (ex1!)))] [(is (= rv :foo)) (is (sm? sv {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id nil}))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! {:catch-val :foo} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (tel/catch->error! {:catch-val :foo ; Overrides `:rethrow?`
|
|
||||||
:rethrow? true} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])
|
|
||||||
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/catch->error! {:catch-val nil
|
|
||||||
:catch-sym my-err
|
|
||||||
:data {:my-err my-err}} (ex1!)))]
|
|
||||||
[(is (= rv nil)) (is (sm? sv {:kind :error, :data {:my-err pex1?}}))])])
|
|
||||||
|
|
||||||
(testing "trace!" ; run + ?id => run result (value or throw)
|
|
||||||
[(let [[[rv] [sv]] (with-sigs (tel/trace! (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id nil, :msg_ "(+ 1 2) => 3"}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/trace! {:msg nil} (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id nil, :msg_ nil}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/trace! :id1 (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id :id1}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/trace! {:id :id1} (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id :id1}))])
|
|
||||||
(let [[[_ re] [sv]] (with-sigs (tel/trace! :id1 (ex1!)))] [(is (ex1? re)) (is (sm? sv {:kind :trace, :line :submap/ex, :level :info, :id :id1, :error pex1?,
|
|
||||||
:msg_ #?(:clj "(ex1!) !> clojure.lang.ExceptionInfo"
|
|
||||||
:cljs "(ex1!) !> cljs.core/ExceptionInfo")}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/trace! {:allow? false} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])
|
|
||||||
(let [[_ [sv1 sv2]]
|
|
||||||
(with-sigs (tel/trace! {:id :id1, :catch->error :id2} (ex1!)))]
|
|
||||||
[(is (sm? sv1 {:kind :trace, :line :submap/ex, :level :info, :id :id1}))
|
|
||||||
(is (sm? sv2 {:kind :error, :line :submap/ex, :level :error, :id :id2}))
|
|
||||||
(is (= (:location sv1) (:location sv2)) "Error inherits exact same location")])])
|
|
||||||
|
|
||||||
(testing "spy" ; run + ?level => run result (value or throw)
|
|
||||||
[(let [[[rv] [sv]] (with-sigs (tel/spy! (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :info, :msg_ "(+ 1 2) => 3"}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/spy! {:msg nil} (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :info, :msg_ nil}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/spy! :warn (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :warn}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/spy! {:level :warn} (+ 1 2)))] [(is (= rv 3)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :warn}))])
|
|
||||||
(let [[[_ re] [sv]] (with-sigs (tel/spy! :warn (ex1!)))] [(is (ex1? re)) (is (sm? sv {:kind :spy, :line :submap/ex, :level :warn, :error pex1?,
|
|
||||||
:msg_ #?(:clj "(ex1!) !> clojure.lang.ExceptionInfo"
|
|
||||||
:cljs "(ex1!) !> cljs.core/ExceptionInfo")}))])
|
|
||||||
(let [[[rv] [sv]] (with-sigs (tel/spy! {:allow? false} (+ 1 2)))] [(is (= rv 3)) (is (nil? sv))])
|
|
||||||
(let [[_ [sv1 sv2]]
|
|
||||||
(with-sigs (tel/spy! {:id :id1, :catch->error :id2} (ex1!)))]
|
|
||||||
[(is (sm? sv1 {:kind :spy, :line :submap/ex, :level :info, :id :id1}))
|
|
||||||
(is (sm? sv2 {:kind :error, :line :submap/ex, :level :error, :id :id2}))
|
|
||||||
(is (= (:location sv1) (:location sv2)) "Error inherits exact same location")])])
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(testing "uncaught->error!"
|
|
||||||
(let [sv_ (atom ::nx)]
|
|
||||||
[(do (enc/set-var-root! impl/*sig-handlers* [(sigs/wrap-handler "h1" (fn h1 [x] (reset! sv_ x)) nil {:async nil})]) :set-handler)
|
|
||||||
;;
|
|
||||||
(is (nil? (tel/uncaught->error!)))
|
|
||||||
(is (do (.join (impl/threaded (ex1!))) (sm? @sv_ {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id nil})))
|
|
||||||
;;
|
|
||||||
(is (nil? (tel/uncaught->error! :id1)))
|
|
||||||
(is (do (.join (impl/threaded (ex1!))) (sm? @sv_ {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id :id1})))
|
|
||||||
;;
|
|
||||||
(is (nil? (tel/uncaught->error! {:id :id1})))
|
|
||||||
(is (do (.join (impl/threaded (ex1!))) (sm? @sv_ {:kind :error, :line :submap/ex, :level :error, :error pex1?, :id :id1})))
|
|
||||||
;;
|
|
||||||
(do (enc/set-var-root! impl/*sig-handlers* nil) :unset-handler)])))])
|
|
||||||
|
|
||||||
;;;; Intake
|
|
||||||
|
|
||||||
(comment (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MyTelemereSLF4JLogger")))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(deftest _intake
|
|
||||||
[(testing "`clojure.tools.logging` -> Telemere"
|
|
||||||
[(is (sm? (tel/check-intakes) {:tools-logging {:present? true, :sending->telemere? true, :telemere-receiving? true}}))
|
|
||||||
|
|
||||||
(is (sm? (with-sig (ctl/info "Hello" "x" "y")) {:level :info, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/tools-logging, :msg_ "Hello x y", :inst pinst?}))
|
|
||||||
(is (sm? (with-sig (ctl/warn "Hello" "x" "y")) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/tools-logging, :msg_ "Hello x y", :inst pinst?}))
|
|
||||||
(is (sm? (with-sig (ctl/error ex1 "An error")) {:level :error, :error pex1?, :inst pinst?}) "Errors")])
|
|
||||||
|
|
||||||
(testing "Standard out/err streams -> Telemere"
|
|
||||||
[(is (sm? (tel/check-intakes) {:system/out {:sending->telemere? false, :telemere-receiving? false},
|
|
||||||
:system/err {:sending->telemere? false, :telemere-receiving? false}}))
|
|
||||||
|
|
||||||
(is (true? (tel/streams->telemere!)))
|
|
||||||
(is (sm? (tel/check-intakes) {:system/out {:sending->telemere? true, :telemere-receiving? true},
|
|
||||||
:system/err {:sending->telemere? true, :telemere-receiving? true}}))
|
|
||||||
|
|
||||||
(is (true? (tel/streams->reset!)))
|
|
||||||
(is (sm? (tel/check-intakes) {:system/out {:sending->telemere? false, :telemere-receiving? false},
|
|
||||||
:system/err {:sending->telemere? false, :telemere-receiving? false}}))
|
|
||||||
|
|
||||||
(is (sm? (with-sig (tel/with-out->telemere (println "Hello" "x" "y")))
|
|
||||||
{:level :info, :location nil, :ns nil, :kind :system/out, :msg_ "Hello x y"}))])
|
|
||||||
|
|
||||||
(testing "SLF4J -> Telemere"
|
|
||||||
[(is (sm? (tel/check-intakes) {:slf4j {:present? true, :sending->telemere? true, :telemere-receiving? true}}))
|
|
||||||
(let [^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MyTelemereSLF4JLogger")]
|
|
||||||
[(testing "Basics"
|
|
||||||
[(is (sm? (with-sig (.info sl "Hello")) {:level :info, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello", :inst pinst?}) "Legacy API: info basics")
|
|
||||||
(is (sm? (with-sig (.warn sl "Hello")) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello", :inst pinst?}) "Legacy API: warn basics")
|
|
||||||
(is (sm? (with-sig (-> (.atInfo sl) (.log "Hello"))) {:level :info, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello", :inst pinst?}) "Fluent API: info basics")
|
|
||||||
(is (sm? (with-sig (-> (.atWarn sl) (.log "Hello"))) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/slf4j, :msg_ "Hello", :inst pinst?}) "Fluent API: warn basics")])
|
|
||||||
|
|
||||||
(testing "Message formatting"
|
|
||||||
(let [msgp "X is {} and Y is {}", expected {:msg_ "X is x and Y is y", :data {:slf4j/args ["x" "y"]}}]
|
|
||||||
[(is (sm? (with-sig (.info sl msgp "x" "y")) expected) "Legacy API: formatted message, raw args")
|
|
||||||
(is (sm? (with-sig (-> (.atInfo sl) (.setMessage msgp) (.addArgument "x") (.addArgument "y") (.log))) expected) "Fluent API: formatted message, raw args")]))
|
|
||||||
|
|
||||||
(is (sm? (with-sig (-> (.atInfo sl) (.addKeyValue "k1" "v1") (.addKeyValue "k2" "v2") (.log))) {:data {:slf4j/kvs {"k1" "v1", "k2" "v2"}}}) "Fluent API: kvs")
|
|
||||||
|
|
||||||
(testing "Markers"
|
|
||||||
(let [m1 (#'slf4j/est-marker! "M1")
|
|
||||||
m2 (#'slf4j/est-marker! "M2")
|
|
||||||
cm (#'slf4j/est-marker! "Compound" "M1" "M2")]
|
|
||||||
|
|
||||||
[(is (sm? (with-sig (.info sl cm "Hello")) {:data #:slf4j{:marker-names #{"Compound" "M1" "M2"}}}) "Legacy API: markers")
|
|
||||||
(is (sm? (with-sig (-> (.atInfo sl) (.addMarker m1) (.addMarker cm) (.log))) {:data #:slf4j{:marker-names #{"Compound" "M1" "M2"}}}) "Fluent API: markers")]))
|
|
||||||
|
|
||||||
(testing "Errors"
|
|
||||||
[(is (sm? (with-sig (.warn sl "An error" ^Throwable ex1)) {:level :warn, :error pex1?}) "Legacy API: errors")
|
|
||||||
(is (sm? (with-sig (-> (.atWarn sl) (.setCause ex1) (.log))) {:level :warn, :error pex1?}) "Fluent API: errors")])
|
|
||||||
|
|
||||||
(testing "MDC (Mapped Diagnostic Context)"
|
|
||||||
(with-open [_ (org.slf4j.MDC/putCloseable "k1" "v1")]
|
|
||||||
(with-open [_ (org.slf4j.MDC/putCloseable "k2" "v2")]
|
|
||||||
[(is (sm? (with-sig (-> sl (.info "Hello"))) {:level :info, :ctx {"k1" "v1", "k2" "v2"}}) "Legacy API: MDC")
|
|
||||||
(is (sm? (with-sig (-> (.atInfo sl) (.log "Hello"))) {:level :info, :ctx {"k1" "v1", "k2" "v2"}}) "Fluent API: MDC")])))])])]))
|
|
||||||
|
|
||||||
;;;; Timbre shim
|
|
||||||
|
|
||||||
(deftest _timbre-shim
|
|
||||||
[(is (sm? (with-sig (timbre/log :warn "x1" nil "x2")) {:kind :log, :level :warn, :id timbre/shim-id, :msg_ "x1 nil x2", :data {:vargs ["x1" nil "x2"]}, :ns pstr?}))
|
|
||||||
(is (sm? (with-sig (timbre/info "x1" nil "x2")) {:kind :log, :level :info, :id timbre/shim-id, :msg_ "x1 nil x2", :data {:vargs ["x1" nil "x2"]}, :ns pstr?}))
|
|
||||||
(is (sm? (with-sig (timbre/error "x1" nil "x2")) {:kind :log, :level :error, :id timbre/shim-id, :msg_ "x1 nil x2", :data {:vargs ["x1" nil "x2"]}, :ns pstr?}))
|
|
||||||
|
|
||||||
(is (sm? (with-sig (timbre/logf :warn "%s %s %s" "x1" nil "x2")) {:kind :log, :level :warn, :id timbre/shim-id, :msg_ "x1 nil x2", :data {:vargs ["x1" nil "x2"]}, :ns pstr?}))
|
|
||||||
(is (sm? (with-sig (timbre/infof "%s %s %s" "x1" nil "x2")) {:kind :log, :level :info, :id timbre/shim-id, :msg_ "x1 nil x2", :data {:vargs ["x1" nil "x2"]}, :ns pstr?}))
|
|
||||||
(is (sm? (with-sig (timbre/errorf "%s %s %s" "x1" nil "x2")) {:kind :log, :level :error, :id timbre/shim-id, :msg_ "x1 nil x2", :data {:vargs ["x1" nil "x2"]}, :ns pstr?}))
|
|
||||||
|
|
||||||
(is (sm? (with-sig (timbre/info ex1 "x1" "x2")) {:kind :log, :level :info, :error pex1?, :msg_ "x1 x2", :data {:vargs ["x1" "x2"]}}) "First-arg error")
|
|
||||||
|
|
||||||
(is (sm? (with-sig (timbre/spy! :info "my-name" (+ 1 2))) {:kind :spy, :level :info, :id timbre/shim-id, :msg_ "my-name => 3", :ns pstr?}))
|
|
||||||
(is (sm? (with-sig (timbre/spy! (+ 1 2))) {:kind :spy, :level :debug, :id timbre/shim-id, :msg_ "(+ 1 2) => 3", :ns pstr?}))
|
|
||||||
(is (sm? (with-sig (timbre/spy! (ex1!))) {:kind :error, :level :error, :id timbre/shim-id, :msg_ nil, :error pex1?, :ns pstr?}))
|
|
||||||
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (timbre/log-errors (ex1!)))] [(is (nil? re)) (is (sm? sv {:kind :error, :level :error, :error pex1?, :id timbre/shim-id}))])
|
|
||||||
(let [[[rv re] [sv]] (with-sigs (timbre/log-and-rethrow-errors (ex1!)))] [(is (ex1? re)) (is (sm? sv {:kind :error, :level :error, :error pex1?, :id timbre/shim-id}))])])
|
|
||||||
|
|
||||||
;;;; Utils
|
|
||||||
|
|
||||||
(deftest _utils
|
|
||||||
[(testing "Basic utils"
|
|
||||||
[(is (= (utils/upper-qn :foo/bar) "FOO/BAR"))
|
|
||||||
|
|
||||||
(is (= (utils/format-level :info) "INFO"))
|
|
||||||
(is (= (utils/format-level 8) "LEVEL:8"))
|
|
||||||
|
|
||||||
(is (= (utils/format-id "foo.bar" :foo.bar/qux) "::qux"))
|
|
||||||
(is (= (utils/format-id "foo.baz" :foo.bar/qux) ":foo.bar/qux"))])
|
|
||||||
|
|
||||||
(testing "error-signal?"
|
|
||||||
[(is (= (utils/error-signal? {:error nil}) false))
|
|
||||||
(is (= (utils/error-signal? {:error ex1}) true))
|
|
||||||
(is (= (utils/error-signal? {:kind :error}) true))
|
|
||||||
(is (= (utils/error-signal? {:level :error}) true))
|
|
||||||
(is (= (utils/error-signal? {:level :fatal}) true))
|
|
||||||
(is (= (utils/error-signal? {:error? true}) true))])
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(testing "File writer"
|
|
||||||
(let [f (java.io.File/createTempFile "file-writer-test" ".txt")
|
|
||||||
fw (utils/file-writer f false)]
|
|
||||||
|
|
||||||
[(is (true? (fw "1")))
|
|
||||||
(is (true? (.delete f)))
|
|
||||||
(do (Thread/sleep 500) :sleep) ; Wait for `exists` cache to clear
|
|
||||||
(is (true? (fw "2")))
|
|
||||||
(is (= (slurp f) "2"))
|
|
||||||
|
|
||||||
(is (true? (.delete f)))
|
|
||||||
(is (true? (.createNewFile f))) ; Can break stream without triggering auto reset
|
|
||||||
|
|
||||||
(is (fw :writer/reset!))
|
|
||||||
(is (true? (fw "3")))
|
|
||||||
(is (= (slurp f) "3"))
|
|
||||||
(is (true? (fw "3")))
|
|
||||||
(is (true? (.delete f)))])))
|
|
||||||
|
|
||||||
(testing "Formatters, etc."
|
|
||||||
[(is (= (utils/error-in-signal->maps {:level :info, :error ex2})
|
|
||||||
{:level :info, :error [{:type ex-info-type, :msg "Ex2", :data {:k2 "v2"}}
|
|
||||||
{:type ex-info-type, :msg "Ex1", :data {:k1 "v1"}}]}))
|
|
||||||
|
|
||||||
(is (= (utils/minify-signal {:level :info, :location {:ns "ns"}, :file "file"}) {:level :info}))
|
|
||||||
(is (= ((utils/format-nsecs-fn) 1.5e9) "1.50s")) ; More tests in Encore
|
|
||||||
(is (= ((utils/format-inst-fn) t0) "2024-06-09T21:15:20.170Z"))
|
|
||||||
|
|
||||||
(is (enc/str-starts-with? ((utils/format-error-fn) ex2)
|
|
||||||
#?(:clj " Root: clojure.lang.ExceptionInfo - Ex1\n data: {:k1 \"v1\"}\n\nCaused: clojure.lang.ExceptionInfo - Ex2\n data: {:k2 \"v2\"}\n\nRoot stack trace:\n"
|
|
||||||
:cljs " Root: cljs.core/ExceptionInfo - Ex1\n data: {:k1 \"v1\"}\n\nCaused: cljs.core/ExceptionInfo - Ex2\n data: {:k2 \"v2\"}\n\nRoot stack trace:\n")))
|
|
||||||
|
|
||||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
|
|
||||||
prelude ((utils/format-signal-prelude-fn) sig)] ; "2024-06-09T21:15:20.170Z INFO EVENT taoensso.telemere-tests(592,35) ::ev-id"
|
|
||||||
[(is (enc/str-starts-with? prelude "2024-06-09T21:15:20.170Z INFO EVENT"))
|
|
||||||
(is (enc/str-ends-with? prelude "::ev-id"))
|
|
||||||
(is (string? (re-find #"taoensso.telemere-tests\(\d+,\d+\)" prelude)))])
|
|
||||||
|
|
||||||
(testing "format-signal->edn-fn"
|
|
||||||
(let [sig (update (with-sig (tel/event! ::ev-id {:inst t0})) :inst enc/inst->udt)
|
|
||||||
sig* (enc/read-edn ((utils/format-signal->edn-fn) sig))]
|
|
||||||
(is
|
|
||||||
(enc/submap? sig*
|
|
||||||
{:schema 1, :kind :event, :id ::ev-id, :level :info,
|
|
||||||
:ns "taoensso.telemere-tests"
|
|
||||||
:inst udt0
|
|
||||||
:line pnat-int?
|
|
||||||
:column pnat-int?}))))
|
|
||||||
|
|
||||||
#?(:cljs
|
|
||||||
(testing "format-signal->json-fn"
|
|
||||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
|
|
||||||
sig* (enc/read-json ((utils/format-signal->json-fn) sig))]
|
|
||||||
(is
|
|
||||||
(enc/submap? sig*
|
|
||||||
{"schema" 1, "kind" "event", "id" "taoensso.telemere-tests/ev-id",
|
|
||||||
"level" "info", "ns" "taoensso.telemere-tests",
|
|
||||||
"inst" t0s
|
|
||||||
"line" pnat-int?
|
|
||||||
"column" pnat-int?})))))
|
|
||||||
|
|
||||||
(testing "format-signal->str-fn"
|
|
||||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))]
|
|
||||||
(is (enc/str-starts-with? ((utils/format-signal->str-fn) sig)
|
|
||||||
"2024-06-09T21:15:20.170Z INFO EVENT"))))])])
|
|
||||||
|
|
||||||
;;;; File handler
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(deftest _file-names
|
|
||||||
[(is (= (fh/get-file-name "/logs/app.log" nil nil false) "/logs/app.log"))
|
|
||||||
(is (= (fh/get-file-name "/logs/app.log" nil nil true) "/logs/app.log"))
|
|
||||||
(is (= (fh/get-file-name "/logs/app.log" "ts" nil true) "/logs/app.log-ts"))
|
|
||||||
(is (= (fh/get-file-name "/logs/app.log" "ts" 1 false) "/logs/app.log-ts.1"))
|
|
||||||
(is (= (fh/get-file-name "/logs/app.log" "ts" 1 true) "/logs/app.log-ts.1.gz"))
|
|
||||||
(is (= (fh/get-file-name "/logs/app.log" nil 1 false) "/logs/app.log.1"))
|
|
||||||
(is (= (fh/get-file-name "/logs/app.log" nil 1 true) "/logs/app.log.1.gz"))]))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(deftest _file-timestamps
|
|
||||||
[(is (= (fh/format-file-timestamp :daily (fh/udt->edy udt0)) "2024-06-09d"))
|
|
||||||
(is (= (fh/format-file-timestamp :weekly (fh/udt->edy udt0)) "2024-06-03w"))
|
|
||||||
(is (= (fh/format-file-timestamp :monthly (fh/udt->edy udt0)) "2024-06-01m"))]))
|
|
||||||
|
|
||||||
(comment (fh/manage-test-files! :create))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(deftest _file-handling
|
|
||||||
[(is (boolean (fh/manage-test-files! :create)))
|
|
||||||
|
|
||||||
(testing "`scan-files`"
|
|
||||||
;; Just checking basic counts here, should be sufficient
|
|
||||||
[(is (= (count (fh/scan-files "test/logs/app1.log" nil nil :sort)) 1) "1 main, 0 parts")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app1.log" :daily nil :sort)) 0) "0 stamped")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app2.log" nil nil :sort)) 6) "1 main, 5 parts (+gz)")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app3.log" nil nil :sort)) 6) "1 main, 5 parts (-gz")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app4.log" nil nil :sort)) 11) "1 main, 5 parts (+gz) + 5 parts (-gz)")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app5.log" nil nil :sort)) 1) "1 main, 0 unstamped")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app5.log" :daily nil :sort)) 5) "5 stamped")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app6.log" nil nil :sort)) 1) "1 main, 0 unstamped")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app6.log" :daily nil :sort)) 25) "5 stamped * 5 parts")
|
|
||||||
(is (= (count (fh/scan-files "test/logs/app6.log" :weekly nil :sort)) 5) "5 stamped")])
|
|
||||||
|
|
||||||
(testing "`archive-main-file!`"
|
|
||||||
[(is (= (let [df (fh/debugger)] (fh/archive-main-file! "test/logs/app1.log" nil nil 2 :gz df) (df))
|
|
||||||
[[:rename "test/logs/app1.log" "test/logs/app1.log.1.gz"]]))
|
|
||||||
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/archive-main-file! "test/logs/app2.log" nil nil 2 :gz df) (df))
|
|
||||||
[[:delete "test/logs/app2.log.5.gz"]
|
|
||||||
[:delete "test/logs/app2.log.4.gz"]
|
|
||||||
[:delete "test/logs/app2.log.3.gz"]
|
|
||||||
[:delete "test/logs/app2.log.2.gz"]
|
|
||||||
[:rename "test/logs/app2.log.1.gz" "test/logs/app2.log.2.gz"]
|
|
||||||
[:rename "test/logs/app2.log" "test/logs/app2.log.1.gz"]]))
|
|
||||||
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/archive-main-file! "test/logs/app3.log" nil nil 2 :gz df) (df))
|
|
||||||
[[:delete "test/logs/app3.log.5"]
|
|
||||||
[:delete "test/logs/app3.log.4"]
|
|
||||||
[:delete "test/logs/app3.log.3"]
|
|
||||||
[:delete "test/logs/app3.log.2"]
|
|
||||||
[:rename "test/logs/app3.log.1" "test/logs/app3.log.2"]
|
|
||||||
[:rename "test/logs/app3.log" "test/logs/app3.log.1.gz"]]))
|
|
||||||
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/archive-main-file! "test/logs/app6.log" :daily "2021-01-01d" 2 :gz df) (df))
|
|
||||||
[[:delete "test/logs/app6.log-2021-01-01d.5.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2021-01-01d.4.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2021-01-01d.3.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2021-01-01d.2.gz"]
|
|
||||||
[:rename "test/logs/app6.log-2021-01-01d.1.gz" "test/logs/app6.log-2021-01-01d.2.gz"]
|
|
||||||
[:rename "test/logs/app6.log" "test/logs/app6.log-2021-01-01d.1.gz"]]))])
|
|
||||||
|
|
||||||
(testing "`prune-archive-files!`"
|
|
||||||
[(is (= (let [df (fh/debugger)] (fh/prune-archive-files! "test/logs/app1.log" nil 2 df) (df)) []))
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/prune-archive-files! "test/logs/app2.log" nil 2 df) (df)) []))
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/prune-archive-files! "test/logs/app5.log" nil 2 df) (df)) []))
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/prune-archive-files! "test/logs/app5.log" :daily 2 df) (df))
|
|
||||||
[[:delete "test/logs/app5.log-2020-01-01d"]
|
|
||||||
[:delete "test/logs/app5.log-2020-01-02d"]
|
|
||||||
[:delete "test/logs/app5.log-2020-02-01d"]]))
|
|
||||||
|
|
||||||
(is (= (let [df (fh/debugger)] (fh/prune-archive-files! "test/logs/app6.log" :daily 2 df) (df))
|
|
||||||
[[:delete "test/logs/app6.log-2020-01-01d.5.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-01d.4.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-01d.3.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-01d.2.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-01d.1.gz"]
|
|
||||||
|
|
||||||
[:delete "test/logs/app6.log-2020-01-02d.5.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-02d.4.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-02d.3.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-02d.2.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-01-02d.1.gz"]
|
|
||||||
|
|
||||||
[:delete "test/logs/app6.log-2020-02-01d.5.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-02-01d.4.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-02-01d.3.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-02-01d.2.gz"]
|
|
||||||
[:delete "test/logs/app6.log-2020-02-01d.1.gz"]])
|
|
||||||
|
|
||||||
"Prune oldest 3 intervals, with 5 parts each")])
|
|
||||||
|
|
||||||
(is (boolean (fh/manage-test-files! :delete)))]))
|
|
||||||
|
|
||||||
;;;; Other handlers
|
|
||||||
|
|
||||||
(deftest _handler-constructors
|
|
||||||
[#?(:default (is (fn? (tel/handler:console))))
|
|
||||||
#?(:cljs (is (fn? (tel/handler:console-raw))))
|
|
||||||
#?(:clj (is (fn? (tel/handler:file))))
|
|
||||||
#?(:clj (is (fn? (otel/handler:open-telemetry-logger))))])
|
|
||||||
|
|
||||||
(comment (def attrs-map otel/signal->attrs-map))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(deftest _open-telemetry
|
|
||||||
[(testing "attr-name"
|
|
||||||
[(is (= (#'otel/attr-name :foo) "foo"))
|
|
||||||
(is (= (#'otel/attr-name :foo-bar-baz) "foo_bar_baz"))
|
|
||||||
(is (= (#'otel/attr-name :foo/bar-baz) "foo.bar_baz"))
|
|
||||||
(is (= (#'otel/attr-name :Foo/Bar-BAZ) "foo.bar_baz"))
|
|
||||||
(is (= (#'otel/attr-name "Foo Bar-Baz") "foo_bar_baz"))
|
|
||||||
(is (= (#'otel/attr-name :x1.x2/x3-x4 :foo/bar-baz)
|
|
||||||
"x1.x2.x3_x4.foo.bar_baz"))])
|
|
||||||
|
|
||||||
(testing "merge-prefix-map"
|
|
||||||
[(is (= (#'otel/merge-prefix-map nil "pf" nil) nil))
|
|
||||||
(is (= (#'otel/merge-prefix-map nil "pf" {}) nil))
|
|
||||||
(is (= (#'otel/merge-prefix-map {"a" "A"} "pf" {:a :A}) {"a" "A", "pf.a" :A}))
|
|
||||||
(is (= (#'otel/merge-prefix-map {} "pf"
|
|
||||||
{:a/b1 "v1" :a/b2 "v2" :nil nil, :map {:k1 "v1"}})
|
|
||||||
|
|
||||||
{"pf.a.b1" "v1", "pf.a.b2" "v2", "pf.nil" nil, "pf.map" {:k1 "v1"}}))])
|
|
||||||
|
|
||||||
(testing "as-attrs"
|
|
||||||
(is (= (str
|
|
||||||
(#'otel/as-attrs
|
|
||||||
{:string "s", :keyword :foo/bar, :long 5, :double 5.0, :nil nil,
|
|
||||||
:longs [5 5.0 5.0],
|
|
||||||
:doubles [5.0 5 5],
|
|
||||||
:bools [true false nil],
|
|
||||||
:mixed [5 "5" nil],
|
|
||||||
:strings ["a" "b" "c"],
|
|
||||||
:map {:k1 "v1"}}))
|
|
||||||
|
|
||||||
"{bools=[true, false, false], double=5.0, doubles=[5.0, 5.0, 5.0], keyword=\":foo/bar\", long=5, longs=[5, 5, 5], map=[[:k1 \"v1\"]], mixed=[5, \"5\", nil], nil=\"nil\", string=\"s\", strings=[\"a\", \"b\", \"c\"]}")))
|
|
||||||
|
|
||||||
(testing "signal->attrs-map"
|
|
||||||
(let [attrs-map #'otel/signal->attrs-map]
|
|
||||||
[(is (= (attrs-map nil { }) {"error" false}))
|
|
||||||
(is (= (attrs-map :attrs {:attrs {:a1 :A1}}) {"error" false, :a1 :A1}))
|
|
||||||
(is
|
|
||||||
(sm?
|
|
||||||
(attrs-map :attrs
|
|
||||||
{:ns "ns"
|
|
||||||
:line 100
|
|
||||||
:file "file"
|
|
||||||
|
|
||||||
:error ex2
|
|
||||||
:kind :event
|
|
||||||
:level :info
|
|
||||||
:id ::id1
|
|
||||||
:uid #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
|
|
||||||
|
|
||||||
:run-form '(+ 3 2)
|
|
||||||
:run-val 5
|
|
||||||
:run-nsecs 100
|
|
||||||
:sample-rate 0.5
|
|
||||||
|
|
||||||
:parent
|
|
||||||
{:id ::parent-id1
|
|
||||||
:uid #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"}
|
|
||||||
|
|
||||||
:attrs {:a1 :A1}})
|
|
||||||
|
|
||||||
{"ns" "ns"
|
|
||||||
"line" 100
|
|
||||||
"file" "file"
|
|
||||||
|
|
||||||
"error" true
|
|
||||||
"exception.type" 'clojure.lang.ExceptionInfo
|
|
||||||
"exception.message" "Ex1"
|
|
||||||
"exception.stacktrace" (enc/pred string?)
|
|
||||||
"exception.data.k1" "v1"
|
|
||||||
|
|
||||||
"kind" :event
|
|
||||||
"level" :info
|
|
||||||
"id" :taoensso.telemere-tests/id1
|
|
||||||
"parent.id" :taoensso.telemere-tests/parent-id1
|
|
||||||
"uid" #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
|
|
||||||
"parent.uid" #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"
|
|
||||||
|
|
||||||
"run.form" '(+ 3 2)
|
|
||||||
"run.val" 5
|
|
||||||
"run.val_type" 'java.lang.Long
|
|
||||||
"run.nsecs" 100
|
|
||||||
"sample" 0.5
|
|
||||||
|
|
||||||
:a1 :A1}))]))]))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
#?(:cljs
|
|
||||||
(defmethod test/report [:cljs.test/default :end-run-tests] [m]
|
|
||||||
(when-not (test/successful? m)
|
|
||||||
;; Trigger non-zero `lein test-cljs` exit code for CI
|
|
||||||
(throw (ex-info "ClojureScript tests failed" {})))))
|
|
||||||
|
|
||||||
#?(:cljs (test/run-tests))
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -89,65 +91,70 @@ And setup your namespace imports:
|
||||||
Telemere is configured sensibly out-the-box.
|
Telemere is configured sensibly out-the-box.
|
||||||
See section [3-Config](./3-Config) for customization.
|
See section [3-Config](./3-Config) for customization.
|
||||||
|
|
||||||
**Default minimum level**: `:info` (signals with lower levels will no-op).
|
**Default minimum level**: `:info` (signals with lower levels will noop).
|
||||||
|
|
||||||
**Default signal handlers**:
|
**Default signal handlers**:
|
||||||
|
|
||||||
> 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 | [clojure.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 | [clojure.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
|
||||||
|
|
||||||
## Create 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 [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) for more info on signal creators.
|
1. They create signals with a different `:kind` value (which can be handy for filtering, etc.).
|
||||||
- See [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) for signal options (shared by all creators).
|
2. They have different positional arguments and/or return values optimised for concise calling in different use cases.
|
||||||
- See relevant docstrings (links above) for usage info.
|
|
||||||
- See [examples.cljc](https://github.com/taoensso/telemere/blob/master/examples.cljc) for REPL-ready examples.
|
|
||||||
|
|
||||||
## Check signals
|
**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
|
||||||
|
|
||||||
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]))
|
||||||
|
|
@ -160,60 +167,53 @@ Use the [`with-signal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/t
|
||||||
|
|
||||||
Both have several options, see their docstrings (links above) for details.
|
Both have several options, see their docstrings (links above) for details.
|
||||||
|
|
||||||
## Filter signals
|
## 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 **compile-time** filter config
|
- 1. Signal **call filters** pass:
|
||||||
2. Signal **creation** is allowed by **runtime** filter config
|
- a. Compile time: sample rate, kind, ns, id, level, when form, rate limit
|
||||||
3. Signal **handling** is allowed by **handler** filter config
|
- b. Runtime: sample rate, kind, ns, id, level, when form, rate limit
|
||||||
4. Signal **middleware** does not suppress the signal (return nil)
|
|
||||||
5. Handler **middleware** does not suppress the signal (return nil)
|
|
||||||
|
|
||||||
For 1-3, filtering may depend on (in order):
|
- 2. Signal **handler filters** pass:
|
||||||
Sample rate → namespace → kind → id → level → when form/fn → rate limit
|
- a. Compile time: not applicable
|
||||||
|
- b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit
|
||||||
|
|
||||||
|
- 3. **Call transform** `(fn [signal]) => ?modified-signal` returns non-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 ...]}
|
||||||
|
|
||||||
;; Deny all signals in matching namespaces
|
;; Disallow all signals in matching namespaces
|
||||||
(t/set-ns-filter! {:deny "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).
|
||||||
- Sample rates are *multiplicative*: if a signal is created with *20%* sampling and a handler handles *50%* of given signals, then *10%* of possible signals will be handled. This multiplicative rate is helpfully reflected in each signal's final `:sample-rate` value.
|
- See [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) for more about filtering.
|
||||||
- See [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) for internal docs on signal flow.
|
|
||||||
- See section [2-Architecture](./2-Architecture) for a flowchart / visual aid.
|
- See section [2-Architecture](./2-Architecture) for a flowchart / visual aid.
|
||||||
|
|
||||||
Runtime signal filters can be configured with:
|
|
||||||
|
|
||||||
| Global | Dynamic | Filters by |
|
|
||||||
| :-------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------- |
|
|
||||||
| [`set-kind-filter!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-kind-filter!) | [`with-kind-filter`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-kind-filter) | Signal kind (`:log`, `:event`, etc.) |
|
|
||||||
| [`set-ns-filter!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-ns-filter!) | [`with-ns-filter`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-ns-filter) | Signal namespace |
|
|
||||||
| [`set-id-filter!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-id-filter!) | [`with-id-filter`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-id-filter) | Signal id |
|
|
||||||
| [`set-min-level`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#set-min-level) | [`with-min-level`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#with-min-level) | Signal level (minimum can be specified by kind and/or ns) |
|
|
||||||
|
|
||||||
- See relevant docstrings (links above) for usage info.
|
|
||||||
- Compile-time filters are controlled by system-level config, see section [3-Config](./3-Config).
|
|
||||||
|
|
||||||
# Internal help
|
# Internal help
|
||||||
|
|
||||||
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) | List of signal creators |
|
| [`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 for signal creators |
|
| [`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 map content |
|
| [`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:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
| [`help:filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) | Signal filtering and transformation |
|
||||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
| [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handlers) | Signal handler management |
|
||||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
| [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) | Signal handler dispatch options |
|
||||||
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
|
| [`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 |
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,9 @@ console/file/queue/db, etc.).
|
||||||
|
|
||||||
So you *call* a *signal creator* to (conditionally) create a *signal* (map) which is then dispatched to registered _signal handlers_ for (conditional) handling.
|
So you *call* a *signal creator* to (conditionally) create a *signal* (map) which is then dispatched to registered _signal handlers_ for (conditional) handling.
|
||||||
|
|
||||||
This flow is described by [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow), and is visualized below:
|
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 configured per handler when calling [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!). Default semantics are: async with a dropping buffer, and 1 handler thread.
|
- `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.).
|
||||||
|
|
||||||
For more info see:
|
|
||||||
|
|
||||||
| Var | Help with |
|
|
||||||
| :-------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
|
|
||||||
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | List of signal creators |
|
|
||||||
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options for signal creators |
|
|
||||||
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal map content |
|
|
||||||
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
|
||||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
|
||||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
|
||||||
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
|
|
||||||
118
wiki/3-Config.md
118
wiki/3-Config.md
|
|
@ -1,20 +1,37 @@
|
||||||
See below for config by topic-
|
See below for config by topic-
|
||||||
|
|
||||||
# Signal 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 **compile-time** filter config
|
- 1. Signal **call filters** pass:
|
||||||
2. Signal **creation** is allowed by **runtime** filter config
|
- a. Compile time: sample rate, kind, ns, id, level, when form, rate limit
|
||||||
3. Signal **handling** is allowed by **handler** filter config
|
- b. Runtime: sample rate, kind, ns, id, level, when form, rate limit
|
||||||
4. Signal **middleware** does not suppress the signal (return nil)
|
|
||||||
5. Handler **middleware** does not suppress the signal (return nil)
|
|
||||||
|
|
||||||
For 1-3, filtering may depend on (in order):
|
- 2. Signal **handler filters** pass:
|
||||||
Sample rate → namespace → kind → id → level → when form/fn → rate limit
|
- a. Compile time: not applicable
|
||||||
|
- b. Runtime: sample rate, kind, ns, id, level, when fn, rate limit
|
||||||
|
|
||||||
- See [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) for info on signal creation filters.
|
- 3. **Call transform** `(fn [signal]) => ?modified-signal` returns non-nil
|
||||||
- See [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) for info on signal handler filters.
|
- 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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|
@ -22,43 +39,43 @@ See section [4-Handlers](./4-Handlers).
|
||||||
|
|
||||||
# Interop
|
# Interop
|
||||||
|
|
||||||
## clojure.tools.logging
|
## tools.logging
|
||||||
|
|
||||||
[`clojure.tools.logging`](https://github.com/clojure/tools.logging) can use Telemere as its logging implementation.
|
[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 `clojure.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 system 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
|
(check-interop) ; =>
|
||||||
[com.taoensso/slf4j-telemere "x.y.z"]
|
|
||||||
```
|
|
||||||
|
|
||||||
When `com.taoensso/slf4j-telemere` is on your classpath AND no other SLF4J backends are, SLF4J will direct all its logging calls to Telemere.
|
|
||||||
|
|
||||||
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}}
|
{: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.
|
> 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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
|
@ -67,53 +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
|
||||||
|
|
||||||
Telemere can send signals as [`LogRecords`](https://opentelemetry.io/docs/specs/otel/logs/data-model/) to [OpenTelemetry](https://opentelemetry.io/).
|
> [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 [`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. 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!).
|
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. 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.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -1,19 +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.).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
# Included handlers
|
# Included handlers
|
||||||
|
|
||||||
The following handlers are included out-the-box:
|
See ✅ links below for **features and usage**,
|
||||||
|
See ❤️ links below to **vote on future handlers**:
|
||||||
|
|
||||||
| Name | Platform | Writes signals to | Writes signals as |
|
| Target (↓) | Clj | Cljs |
|
||||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- |
|
| :--------------------------------------------- | :-----------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------: |
|
||||||
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | String ([edn](https://github.com/edn-format/edn), JSON, formatted, etc.) |
|
| [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 | String ([edn](https://github.com/edn-format/edn), JSON, formatted, etc.) |
|
| [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 data (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 | String ([edn](https://github.com/edn-format/edn), JSON, formatted, etc.) |
|
| 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 client](https://github.com/open-telemetry/opentelemetry-java) | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) |
|
| [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) | - |
|
||||||
- See relevant docstrings (links above) for more info.
|
| File/s | [✅](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | - |
|
||||||
- See section [8-Community](8-Community.md) for additional handlers.
|
| [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) | - |
|
||||||
|
|
||||||
# Configuring handlers
|
# Configuring handlers
|
||||||
|
|
||||||
|
|
@ -24,21 +48,21 @@ There's two kinds of config relevant to all signal handlers:
|
||||||
|
|
||||||
## Dispatch opts
|
## Dispatch opts
|
||||||
|
|
||||||
Dispatch opts includes dispatch priority, handler filtering, handler middleware, 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.
|
||||||
|
|
||||||
This is all specified when calling [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) - and documented there.
|
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 the 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.
|
||||||
|
|
||||||
|
|
@ -47,49 +71,81 @@ 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
|
||||||
{:format-signal-fn (taoensso.telemere.utils/format-signal->edn-fn)}))
|
{: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 JSON
|
;; Ref. <https://github.com/metosin/jsonista> (or any alt JSON lib)
|
||||||
|
#?(:clj (require '[jsonista.core :as jsonista]))
|
||||||
(def my-handler
|
(def my-handler
|
||||||
(t/handler:console
|
(tel/handler:console
|
||||||
{:format-signal-fn
|
{:output-fn
|
||||||
(taoensso.telemere.utils/format-signal->json-fn
|
(tel/pr-signal-fn
|
||||||
{:pr-json-fn jsonista.core/write-value-as-string})}))
|
{:pr-fn
|
||||||
|
#?(:cljs :json ; Use js/JSON.stringify
|
||||||
|
: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", ...}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that when writing JSON with Clojure, you *must* specify a `pr-json-fn`. This lets you plug in the JSON serializer of your choice ([jsonista](https://github.com/metosin/jsonista) is my default recommendation).
|
Note that when writing JSON with Clojure, you *must* provide an appropriate `pr-fn`. This lets you plug in the JSON serializer of your choice ([jsonista](https://github.com/metosin/jsonista) is my default recommendation).
|
||||||
|
|
||||||
|
### Handler-specific per-signal kvs
|
||||||
|
|
||||||
|
Telemere includes a handy mechanism for including arbitrary 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.:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(tel/with-signal
|
||||||
|
(tel/log!
|
||||||
|
{...
|
||||||
|
:my-data-for-xfn "foo"
|
||||||
|
:my-data-for-handler "bar"}))
|
||||||
|
|
||||||
|
;; %>
|
||||||
|
;; {;; App-level kvs included inline (assoc'd to signal root)
|
||||||
|
;; :my-data-for-xfn "foo"
|
||||||
|
;; :my-data-for-handler "bar"
|
||||||
|
;; :kvs ; And also collected together under ":kvs" key
|
||||||
|
;; {:my-data-for-xfn "foo"
|
||||||
|
;; :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 transforms/handlers.
|
||||||
|
|
||||||
# Managing handlers
|
# Managing handlers
|
||||||
|
|
||||||
See [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) for info on handler management.
|
See [`help:handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) for info on signal handler management.
|
||||||
|
|
||||||
## Managing handlers on startup
|
## Managing handlers on startup
|
||||||
|
|
||||||
|
|
@ -97,88 +153,130 @@ Want to add or remove a particular handler when your application starts?
|
||||||
|
|
||||||
Just make an appropriate call to [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) or [`remove-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#remove-handler!).
|
Just make an appropriate call to [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!) or [`remove-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#remove-handler!).
|
||||||
|
|
||||||
## System-level config
|
### Environmental config
|
||||||
|
|
||||||
If you want to manage handlers **conditionally** based on **system-level config** (e.g. JVM prop, ENV var, or classpath resource) - Telemere provides the highly flexible [`get-env`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-env) util.
|
If you want to manage handlers **conditionally** based on **environmental config** (JVM properties, environment variables, or classpath resources) - Telemere provides the highly flexible [`get-env`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-env) util.
|
||||||
|
|
||||||
Use this to easily check your own cross-platform system config, and make whatever conditional handler management decisions you'd like.
|
Use this to easily define your own arbitrary cross-platform config, and make whatever conditional handler management decisions you'd like.
|
||||||
|
|
||||||
|
## Stopping handlers
|
||||||
|
|
||||||
|
Telemere supports complex handlers that may use internal state, buffers, etc.
|
||||||
|
|
||||||
|
For this reason, you should **always manually call** [`stop-handlers!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#stop-handlers!) somewhere appropriate to give registered handlers the opportunity to flush buffers, close files, etc.
|
||||||
|
|
||||||
|
The best place to do this is usually near the end of your application's `-main` or shutdown procedure, **AFTER** all other code has completed that could create signals.
|
||||||
|
|
||||||
|
You can use [`call-on-shutdown!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#call-on-shutdown!) to create a JVM shutdown hook.
|
||||||
|
|
||||||
|
Note that `stop-handlers!` will conveniently **block** to finish async handling of any pending signals. The max blocking time can be configured *per-handler* via the `:drain-msecs` [handler dispatch option](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) and defaults to 6 seconds.
|
||||||
|
|
||||||
|
## Handler stats
|
||||||
|
|
||||||
|
By default, Telemere handlers maintain **comprehensive internal stats** including handling times and outcome counters.
|
||||||
|
|
||||||
|
This can be **really useful** for debugging handlers, and understanding handler performance and back-pressure behaviour in practice.
|
||||||
|
|
||||||
|
See [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats) for an output example, etc.
|
||||||
|
|
||||||
# Writing handlers
|
# Writing handlers
|
||||||
|
|
||||||
Writing your own signal handlers for Telemere is straightforward, and a reasonable choice if you prefer customizing behaviour that way, or want to write signals to a DB/format/service for which a ready-made handler isn't available.
|
Writing your own signal handlers for Telemere is straightforward, and a reasonable choice if you prefer customizing behaviour that way, or want to write signals to a DB/format/service for which a ready-made handler isn't available.
|
||||||
|
|
||||||
Remember that signals are just plain Clojure/Script [maps](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content), and handlers just plain Clojure/Script functions that do something with those maps.
|
- Signals are just plain Clojure/Script [maps](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content).
|
||||||
|
- Handlers just plain Clojure/Script fns of 2 arities:
|
||||||
Here's a simple Telemere handler:
|
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(fn my-handler [signal] (println signal))
|
(defn my-handler
|
||||||
|
([signal] (println signal)) ; Arity-1 called when handling a signal
|
||||||
|
([] (my-stop-code)) ; Arity-0 called when stopping the handler
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
For more complex cases, or for handlers that you want to make available for use by other folks, here's the general template that Telemere uses for all its included handlers:
|
If you're making a customizable handler for use by others, it's often handy to define a handler **constructor**:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(defn handler:my-handler ; Note naming convention
|
(defn handler:my-fancy-handler ; Note constructor naming convention
|
||||||
"Returns a (fn handler [signal] that:
|
"Needs `some-lib`, Ref. <https://github.com/example/some-lib>.
|
||||||
- Does something.
|
|
||||||
|
Returns a signal handler that:
|
||||||
|
- Takes a Telemere signal (map).
|
||||||
|
- Does something useful with the signal!
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
`:option1` - Description
|
`:option1` - Option description
|
||||||
`:option2` - Description"
|
`:option2` - Option description
|
||||||
|
|
||||||
([] (handler:my-handler nil)) ; Use default opts
|
Tips:
|
||||||
|
- Tip 1
|
||||||
|
- Tip 2"
|
||||||
|
|
||||||
|
([] (handler:my-fancy-handler nil)) ; Use default opts (iff defaults viable)
|
||||||
([{:as constructor-opts}]
|
([{:as constructor-opts}]
|
||||||
|
|
||||||
;; Do expensive prep outside returned handler fn whenever possible -
|
;; Do option validation and other prep here, i.e. try to keep
|
||||||
;; i.e. at (one-off) construction time rather than handling time.
|
;; expensive work outside handler function when possible!
|
||||||
(let []
|
|
||||||
|
|
||||||
(fn a-handler:my-handler ; Note naming convention
|
(let [handler-fn ; Fn of exactly 2 arities (1 and 0)
|
||||||
|
(fn a-handler:my-fancy-handler ; Note fn naming convention
|
||||||
|
|
||||||
;; Shutdown arity - called by Telemere exactly once when the handler is
|
([signal] ; Arity-1 called when handling a signal
|
||||||
;; to be shut down. This is your opportunity to finalize/free resources, etc.
|
;; Do something useful with the given signal (write to
|
||||||
([])
|
;; console/file/queue/db, etc.). Return value is ignored.
|
||||||
|
)
|
||||||
|
|
||||||
;; Main arity - called by Telemere whenever the handler should handle the
|
([] ; Arity-0 called when stopping the handler
|
||||||
;; given signal. Never called after shutdown.
|
;; Flush buffers, close files, etc. May just noop.
|
||||||
([signal]
|
;; Return value is ignored.
|
||||||
;; TODO Do something with given signal
|
))]
|
||||||
)))))
|
|
||||||
|
;; (Advanced, optional) You can use metadata to provide default
|
||||||
|
;; handler dispatch options (see `help:handler-dispatch-options`)
|
||||||
|
|
||||||
|
(with-meta handler-fn
|
||||||
|
{:dispatch-opts
|
||||||
|
{:min-level :info
|
||||||
|
:limit
|
||||||
|
[[1 1000] ; Max 1 signal per second
|
||||||
|
[10 60000] ; Max 10 signals per minute
|
||||||
|
]}}))))
|
||||||
```
|
```
|
||||||
|
|
||||||
- See [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) for signal map content.
|
- See [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) for signal map content.
|
||||||
|
- See [`help:handler-dispatch-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:handler-dispatch-options) for dispatch options.
|
||||||
- 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.
|
||||||
- See section [8-Community](8-Community.md) for PRs to link to community-authored handlers.
|
- 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).
|
||||||
|
- [PRs](https://github.com/taoensso/telemere/pulls) are **very welcome** for additions to Telemere's included handlers, or to Telemere's [community resources](./8-Community)!
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
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}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cljs console handler
|
## Cljs console handler
|
||||||
|
|
||||||
Chrome console:
|
[API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Chrome console:
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-cljs-console.png" alt="Default ClojureScript console handler output" width="640"/>
|
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-cljs-console.png" alt="Default ClojureScript console handler output" width="640"/>
|
||||||
|
|
||||||
## Cljs raw console handler
|
## Cljs raw console handler
|
||||||
|
|
||||||
Chrome console, with [cljs-devtools](https://github.com/binaryage/cljs-devtools):
|
[API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Chrome console, with [cljs-devtools](https://github.com/binaryage/cljs-devtools):
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-cljs-console-raw.png" alt="Raw ClojureScript console handler output" width="640"/>
|
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-cljs-console-raw.png" alt="Raw ClojureScript console handler output" width="640"/>
|
||||||
|
|
||||||
## Clj file handler
|
## Clj file handler
|
||||||
|
|
||||||
MacOS terminal:
|
[API](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | MacOS terminal:
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-clj-file.png" alt="Default Clojure file handler output" width="640"/>
|
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/handler-output-clj-file.png" alt="Default Clojure file handler output" width="640"/>
|
||||||
|
|
@ -4,13 +4,13 @@ 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:
|
||||||
|
|
||||||
- Significantly better performance
|
- Better support for [structured logging](./1-Getting-started#data-types-and-structures)
|
||||||
- A cleaner and more flexible API
|
- Better [performance](https://github.com/taoensso/telemere#performance)
|
||||||
- Better support for structured logging
|
- Better [documentation](https://github.com/taoensso/telemere#documentation)
|
||||||
- Much better documentation
|
- A more flexible [API](./1-Getting-started#usage) that unifies all telemetry and logging needs
|
||||||
- Better built-in handlers
|
- A more robust [architecture](./2-Architecture), free from all historical constraints
|
||||||
- Easier configuration in many cases
|
- Better [included handlers](./4-Handlers##included-handlers)
|
||||||
- A more robust architecture, free from all historical constraints
|
- 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,17 +20,29 @@ 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).
|
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).
|
||||||
|
|
||||||
This may be easier than it sounds. Remember that signals are just plain Clojure/Script [maps](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content), and handlers just plain Clojure/Script functions that do something with those maps.
|
This may be easier than it sounds. Remember that signals are just plain Clojure/Script [maps](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content), and handlers just plain Clojure/Script functions that do something with those maps.
|
||||||
|
|
||||||
Feel free to [ping me](https://github.com/taoensso/telemere/issues) for assistance, or ask on the [`#telemere` Slack channel](https://clojurians.slack.com/archives/C06ALA6EEUA).
|
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
|
||||||
|
|
||||||
|
|
|
||||||
118
wiki/6-FAQ.md
118
wiki/6-FAQ.md
|
|
@ -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,29 +12,29 @@ 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?
|
||||||
|
|
||||||
Timbre was first released 12+ years ago, and has mostly attempted to keep breaks in that time minimal. Which means that its fundamental design is now 12+ years old.
|
Timbre was first released 12+ years ago, and has mostly attempted to keep breaks in that time minimal. Which means that its fundamental design is now 12+ years old.
|
||||||
|
|
||||||
I've learnt a lot since then, and would write Timbre differently if I were doing it again today. There's many improvements I've wanted to make over the years, but held back both because of the effort involved and because of not wanting to break Timbre users that are happy with it the way it is.
|
I've learnt a lot since then, and would write Timbre differently if I were doing it again today. There's many refinements I've wanted to make over the years, but held back both because of the effort involved and because of not wanting to break Timbre users that are happy with it the way it is.
|
||||||
|
|
||||||
Since receiving [open source funding](https://www.taoensso.com/my-work), undertaking larger projects became feasible - so I decided to experiment with a proof-of-concept rewrite free of all historical constraints.
|
Since receiving [open source funding](https://www.taoensso.com/my-work), undertaking larger projects became feasible - so I decided to experiment with a proof-of-concept rewrite free of all historical constraints.
|
||||||
|
|
||||||
That eventually grew into Telemere.
|
That eventually grew into Telemere. And I'm happy enough with the result that I feel confident in saying that there's nothing Timbre does better than Telemere, but plenty that Telemere does better than Timbre. Telemere is easier to use, faster, more robust, and significantly more flexible. It offers a better platform for what will be (I hope) the next many years of service.
|
||||||
|
|
||||||
I will **continue to maintain and support** Timbre for users that are happy with it, though I've also tried to make [migration](./5-Migrating#from-timbre) as easy as possible.
|
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.
|
||||||
|
|
||||||
|
|
@ -50,6 +50,20 @@ They're focused on complementary things. When both are in use:
|
||||||
- Tufte can be used for detailed performance measurement, and
|
- Tufte can be used for detailed performance measurement, and
|
||||||
- Telemere can be used for conveying (aggregate) performance information as part of your system's general observability signals.
|
- Telemere can be used for conveying (aggregate) performance information as part of your system's general observability signals.
|
||||||
|
|
||||||
|
# Does Telemere work with GraalVM?
|
||||||
|
|
||||||
|
> [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.
|
||||||
|
|
||||||
|
# Does Telemere work with Babashka?
|
||||||
|
|
||||||
|
> [Babashka](https://github.com/babashka/babashka) is a native Clojure interpreter for scripting with fast startup.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
# Why no format-style messages?
|
# Why no format-style messages?
|
||||||
|
|
||||||
Telemere's message API can do everything that traditional print *or* format style message builders can do but **much more flexibly** - and with pure Clojure/Script (so no arcane pattern syntax).
|
Telemere's message API can do everything that traditional print *or* format style message builders can do but **much more flexibly** - and with pure Clojure/Script (so no arcane pattern syntax).
|
||||||
|
|
@ -62,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)]
|
||||||
|
|
@ -86,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`"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -97,6 +111,84 @@ Note that you can even use `format` or any other formatter/s of your choice. You
|
||||||
|
|
||||||
See also [`msg-skip`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#msg-skip) and [`msg-splice`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#msg-splice) for some handy utils.
|
See also [`msg-skip`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#msg-skip) and [`msg-splice`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#msg-splice) for some handy utils.
|
||||||
|
|
||||||
|
# How to use Telemere from a library?
|
||||||
|
|
||||||
|
See section [9-Authors](./9-Authors.md).
|
||||||
|
|
||||||
|
# How does Telemere compare to μ/log?
|
||||||
|
|
||||||
|
> [μ/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 μ/log:
|
||||||
|
|
||||||
|
- Both emphasize **structured data** rather than string messages
|
||||||
|
- Both offer **tracing** to understand (nested) program flow
|
||||||
|
- Both offer a (nested) **context** mechanism for arb application state
|
||||||
|
- Both are **fast** and offer **async handling**
|
||||||
|
- Both offer a variety of **handlers** and are designed for ease of use
|
||||||
|
|
||||||
|
Some particular **strengths of μ/log** that I'm aware of:
|
||||||
|
|
||||||
|
- More **established/mature**
|
||||||
|
- Wider **range of handlers** (incl. Kafka, Kinesis, Prometheus, Zipkin, etc.)
|
||||||
|
- More **community resources** (videos, guides, users, etc.)
|
||||||
|
- **Smaller code** base (Telemere currently depends on [Encore](https://github.com/taoensso/encore))
|
||||||
|
- There may be others!
|
||||||
|
|
||||||
|
Some particular **strengths of Telemere**:
|
||||||
|
|
||||||
|
- 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 **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
|
||||||
|
- Detailed **handler stats** (see [`get-handlers-stats`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#get-handlers-stats))
|
||||||
|
- Single **unified API** for all telemetry and traditional logging needs (see [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators))
|
||||||
|
- Lazy `:let`, `:data`, `:msg`, `:do` - evaluated only **after filtering**
|
||||||
|
- Extensive [in-IDE documentation](./1-Getting-started#internal-help)
|
||||||
|
|
||||||
|
**My subjective thoughts**:
|
||||||
|
|
||||||
|
μ/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.
|
||||||
|
|
||||||
|
Ultimately I wrote Telemere because:
|
||||||
|
|
||||||
|
1. I have some particular needs, including very complex and large-scale applications that benefit from the kind of flexibility that Telemere offers re: filtering, dispatch, environmental config, lazy (post-filter) evaluation, etc.
|
||||||
|
2. I have some particular tastes re: my ideal API.
|
||||||
|
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).
|
||||||
|
|
||||||
|
# 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 doesn’t read well IMO. I want to know what event we’re 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, it’s 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 there’s popular demand, I’d also be happy to add something like `ev!` which could choose the alternative trade-off. Though I’d recommend folks try `event!` as-is first, since I think the initial aversion/surprise might wear off with use.
|
||||||
|
|
||||||
# Other questions?
|
# Other questions?
|
||||||
|
|
||||||
Please [open a Github issue](https://github.com/taoensso/telemere/issues). I'll regularly update the FAQ to add common 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)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ Consider the **quantities** of data that'd best suit your needs *for that data*:
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/signal-sampling.svg" alt="Telemere sampling" width="640"/>
|
<img src="https://raw.githubusercontent.com/taoensso/telemere/master/imgs/signal-sampling.svg" alt="Telemere sampling" width="640"/>
|
||||||
|
|
||||||
Telemere offers [extensive filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) capabilities that help you easily express the **conditions** and **quantities** that make sense for your needs. *Use these* both for their effects and as a *form of documentation*.
|
Telemere offers [extensive filtering](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:filters) capabilities that help you easily express the **conditions** and **quantities** that make sense for your needs. *Use these* both for their effects and as a *form of documentation*.
|
||||||
|
|
||||||
## Consider evolution
|
## Consider evolution
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ Consider the [differences](https://www.youtube.com/watch?v=oyLBGkS5ICk) between
|
||||||
|
|
||||||
- [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) and [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) are both **good general-purpose** signal creators.
|
- [`log!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#log!) and [`event!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#event!) are both **good general-purpose** signal creators.
|
||||||
|
|
||||||
- Try **always provide an id** for all signals you create.
|
- **Provide an id** for all signals you create.
|
||||||
|
|
||||||
Qualified keywords are perfect! Downstream behaviour (e.g. alerts) can then look for these ids rather than messages (which are harder to match and more likely to change).
|
Qualified keywords are perfect! Downstream behaviour (e.g. alerts) can then look for these ids rather than messages (which are harder to match and more likely to change).
|
||||||
|
|
||||||
|
|
@ -80,23 +80,26 @@ 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 sampling** and **handler sampling** are **multiplicative**.
|
- Signal and handler **sampling is multiplicative**.
|
||||||
|
|
||||||
If a signal is created with *20%* sampling and a handler handles *50%* of given signals, then *10%* of possible signals will be handled.
|
Both signals and handlers can have independent sample rates, and these MULTIPLY!
|
||||||
|
|
||||||
This multiplicative rate is helpfully reflected in each signal's final `:sample-rate` value, making it possible to estimate unsampled cardinalities in relevant cases.
|
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%).
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
- Middleware can return any type, but it's best to return only `nil` or a map.
|
- 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 be used to **filter signals** by returning `nil`.
|
|
||||||
- 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.
|
- Transforms can be used to **filter signals** by returning `nil`.
|
||||||
|
- Transforms can be used to **split signals**:
|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
|
|
@ -106,18 +109,18 @@ Consider the [differences](https://www.youtube.com/watch?v=oyLBGkS5ICk) between
|
||||||
|
|
||||||
Telemere doesn't couple the presence of an error value to signal level. This can be handy, but means that you need to be clear on what constitutes an "error signal" for your use case. See also the [`error-signal?`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error-signal) util.
|
Telemere doesn't couple the presence of an error value to signal level. This can be handy, but means that you need to be clear on what constitutes an "error signal" for your use case. See also the [`error-signal?`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#error-signal) util.
|
||||||
|
|
||||||
- Signals may contain arbitrary user-level keys.
|
- Signals may contain arbitrary app-level keys.
|
||||||
|
|
||||||
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 user 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.
|
||||||
|
|
||||||
User kvs are a great way of controlling the per-signal behaviour of custom/advanced 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,27 @@
|
||||||
My plan is for Telemere to offer a **stable core of limited scope**.
|
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.
|
||||||
|
|
||||||
If there's demand, additional stuff can then be authored by Telemere's *community*.
|
[PRs](../wiki#contributions-welcome) **very welcome** to add links to this page!
|
||||||
|
|
||||||
**PRs very welcome** to add links to this page for:
|
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 (see [Writing handlers](./4-Handlers#writing-handlers))
|
# Learning
|
||||||
- Handler utils (formatters, etc.)
|
|
||||||
- Middleware
|
|
||||||
- Tutorials / demos / etc.
|
|
||||||
- Anything else relevant :-)
|
|
||||||
|
|
||||||
If you spot issues with any linked resources, please **contact the relevant authors** to let them know! Thank you! 🙏
|
Includes videos, tutorials, demo projects, etc.
|
||||||
|
|
||||||
| Contributor | Link | Description |
|
| Type | Description |
|
||||||
| :--------------------------------------------- | :-------------------------------------------------------------------------- | :------------------------------------------------------------ |
|
| ------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [@ptaoussanis](https://github.com/ptaoussanis) | [Official Slack channel](https://clojurians.slack.com/archives/C06ALA6EEUA) | For questions, support, etc. |
|
| Support | [Official Slack channel](https://www.taoensso.com/telemere/slack) for questions, help, etc. |
|
||||||
| [@ptaoussanis](https://github.com/ptaoussanis) | [GitHub issues](https://github.com/taoensso/telemere/issues) | For questions, support, bug reports, PRs, etc. |
|
| Support | [Official GitHub issues](https://github.com/taoensso/telemere/issues) for questions, help, bug reports, PRs, etc. |
|
||||||
| _ | _ | Your link here? [PRs](../wiki#contributions-welcome) welcome! |
|
| Example | [Gist](https://gist.github.com/ptaoussanis/f8a80f85d3e0f89b307a470ce6e044b5) showing use with [Bling](https://github.com/paintparty/bling) (2024-12-23) |
|
||||||
| [@username](https://github.com/username) | [Project](https://github.com/username/project) | Short description of resource |
|
| Example | [Gist](https://gist.github.com/xlfe/e9e2cf23bd1dddcbb2fbd77ce31dcc8b) showing use with **Google Cloud Platform** (GCP) (2024-10-13) |
|
||||||
|
| 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
49
wiki/9-Authors.md
Normal 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.
|
||||||
Loading…
Reference in a new issue