GithubHelp home page GithubHelp logo

taoensso / telemere Goto Github PK

View Code? Open in Web Editor NEW
131.0 7.0 2.0 1.6 MB

Structured telemetry library for Clojure/Script

Home Page: https://www.taoensso.com/telemere

License: Eclipse Public License 1.0

Clojure 96.59% Java 3.28% HTML 0.11% Shell 0.02%
clojure clojurescript epl taoensso logging monitoring telemetry instrumentation metrics observability open-telemetry opentelemetry tracing

telemere's Introduction

Taoensso open source
API | Wiki | Latest releases | Slack channel

Telemere logo

Structured telemetry library for Clojure/Script

Telemere is a next-generation replacement for Timbre that offers a simple unified API for structured and traditional logging, tracing, and basic performance monitoring.

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, Tufte, Truss, etc.

See here for full introduction.

Latest release/s

  • 2024-08-23 v1.0.0-beta20: release info (for early adopters/feedback)

Main tests Graal tests

See here for earlier releases.

Quick examples

(require '[taoensso.telemere :as t])

;; Without structured data
(t/log! :info "Hello world!") ; %> Basic log   signal (has message)
(t/event! ::my-id :debug)     ; %> Basic event signal (just id)

;; With structured data
(t/log! {:level :info, :data {...}} "Hello again!")
(t/event! ::my-id {:level :debug, :data {...}})

;; Trace (auto interop with OpenTelemetry)
(t/trace! {:id ::my-id :data {...}}
  (do-some-work))

;; Check signal content for debug/tests
(t/with-signal (t/event! ::my-id)) ; => {:keys [ns level id data msg_ ...]}

;; Transform signals
(t/set-middleware! (fn [signal] (assoc signal :my-key "my-val")))

;; Filter signals by returning nil
(t/set-middleware! (fn [signal] (when-not (-> signal :data :skip-me?) signal)))

;; Getting fancy (all costs are conditional!)
(t/log!
  {:level       :debug
   :sample-rate (my-dynamic-sample-rate)
   :when        (my-conditional)
   :rate-limit  {"1 per sec" [1  1000]
                 "5 per min" [5 60000]}

   :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])

Why Telemere?

Ergonomics

  • Elegant, lightweight API that's easy to use, easy to configure, and deeply flexible.
  • Sensible defaults to make getting started fast and easy.
  • Extensive beginner-oriented documentation, docstrings, and error messages.

Interop

Scaling

  • Hyper-optimized and blazing fast, see benchmarks.
  • An API that scales comfortably from the smallest disposable code, to the most massive and complex real-world production environments.
  • Auto handler stats for debugging performance and other issues at scale.

Flexibility

  • Config via plain Clojure vals and fns for easy customization, composition, and REPL debugging.
  • Unmatched environmental config support: JVM properties, environment variables, or classpath resources. Per platform, or cross-platform.
  • Unmatched filtering support: by namespace, id pattern, level, level by namespace pattern, etc. At runtime and compile-time.
  • Fully configurable a/sync dispatch support: blocking, dropping, sliding, etc.
  • Turn-key sampling, rate-limiting, and back-pressure monitoring with sensible defaults.

Comparisons

Next-gen observability

A key hurdle in building observable systems is that it's often inconvenient and costly to get out the kind of detailed info that we need when debugging.

Telemere's strategy to address this is to:

  1. Provide lean, low-fuss syntax to let you conveniently convey program state.
  2. Use the unique power of Lisp macros to let you dynamically filter costs as you filter signals (pay only for what you need, when you need it).
  3. For those signals that do pass filtering: move costs from the callsite to a/sync handlers with explicit threading and back-pressure semantics and performance monitoring.

The effect is more than impressive micro-benchmarks. This approach enables a fundamental (qualitative) change in one's approach to observability.

It enables you to write code that is information-verbose by default.

Video demo

See for intro and basic usage:

Telemere demo video

More examples

;; Set minimum level
(t/set-min-level!       :warn) ; For all    signals
(t/set-min-level! :log :debug) ; For `log!` signals only

;; Set namespace and id filters
(t/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"})
(t/set-id-filter! {:allow #{::my-particular-id "my-app/*"}})

;; Set minimum level for `event!` signals for particular ns pattern
(t/set-min-level! :event "taoensso.sente.*" :warn)

;; See `t/help:filters` docstring for more

;; Use middleware to:
;;   - Transform signals
;;   - Filter    signals by arb conditions (incl. data/content)

(t/set-middleware!
  (fn [signal]
    (if (-> signal :data :skip-me?)
      nil ; Filter signal (don't handle)
      (assoc signal :passed-through-middleware? true))))

(t/with-signal (t/event! ::my-id {:data {:skip-me? true}}))  ; => nil
(t/with-signal (t/event! ::my-id {:data {:skip-me? false}})) ; => {...}

;; Signal handlers

(t/get-handlers) ; => {<handler-id> {:keys [handler-fn handler-stats_ dispatch-opts]}}

(t/add-handler! :my-console-handler
  (t/handler:console {}) ; Returns handler fn, has many opts
  {:async {:mode :dropping, :buffer-size 1024, :n-threads 1}
   :priority    100
   :sample-rate 0.5
   :min-level   :info
   :ns-filter   {:disallow "taoensso.*"}
   :rate-limit  {"1 per sec" [1 1000]}
   ;; See `t/help:handler-dispatch-options` for more
   })

;; Print human-readable output to console
(t/add-handler! :my-console-handler
  (t/handler:console
    {:output-fn (t/format-signal-fn {...})}))

;; Print edn to console
(t/add-handler! :my-console-handler
  (t/handler:console
    {:output-fn (t/pr-signal-fn {:pr-fn :edn})}))

;; Print JSON to console
;; Ref.  <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista]))
(t/add-handler! :my-console-handler
  (t/handler:console
    {:output-fn
     #?(:cljs :json ; Use js/JSON.stringify
        :clj  jsonista/write-value-as-string)}))

See examples.cljc for REPL-ready snippets!

API overview

See relevant docstrings (links below) for usage info-

Creating signals

Name Signal kind Main arg Optional arg Returns
log! :log msg opts/level Signal allowed?
event! :event id opts/level Signal allowed?
error! :error error opts/id Given error
trace! :trace form opts/id Form result
spy! :spy form opts/level Form result
catch->error! :error form opts/id Form value or given fallback
signal! <arb> opts - Depends on opts

Internal help

Detailed help is available without leaving your IDE:

Var Help with
help:signal-creators Creating signals
help:signal-options Options when creating signals
help:signal-content Signal content (map given to middleware/handlers)
help:filters Signal filtering and transformation
help:handlers Signal handler management
help:handler-dispatch-options Signal handler dispatch options
help:environmental-config Config via JVM properties, environment variables, or classpath resources

Included handlers

See โœ… links below for features and usage,
See ๐Ÿ‘ links below to vote on future handlers:

Target (โ†“) Clj Cljs
Apache Kafka ๐Ÿ‘ -
AWS Kinesis ๐Ÿ‘ -
Console โœ… โœ…
Console (raw) - โœ…
Datadog ๐Ÿ‘ ๐Ÿ‘
Email โœ… -
Graylog ๐Ÿ‘ -
Jaeger ๐Ÿ‘ -
Logstash ๐Ÿ‘ -
OpenTelemetry โœ… ๐Ÿ‘
Redis ๐Ÿ‘ -
SQL ๐Ÿ‘ -
Slack โœ… -
TCP socket โœ… -
UDP socket โœ… -
Zipkin ๐Ÿ‘ -

You can also easily write your own 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, middleware, and utils.

See here for community resources.

Documentation

Benchmarks

Telemere is highly optimized and offers great performance at any scale:

Compile-time filtering? Runtime filtering? Profile? Trace? nsecs
โœ“ (elide) - - - 0
- โœ“ - - 350
- โœ“ โœ“ - 450
- โœ“ โœ“ โœ“ 1000

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 Clojure v1.12 and OpenJDK v22

Performance philosophy

Telemere is optimized for real-world performance. This means prioritizing flexibility and realistic usage over synthetic micro-benchmarks.

Large applications can produce absolute heaps of data, not all equally valuable. Quickly processing infinite streams of unmanageable junk is an anti-pattern. As scale and complexity increase, it becomes more important to strategically plan what data to collect, when, in what quantities, and how to manage it.

Telemere is designed to help with all that. It offers rich data and unmatched filtering support - including per-signal and per-handler sampling and rate-limiting.

Use these to ensure that you're not capturing useless/low-value/high-noise information in production! With appropriate planning, Telemere is designed to scale to systems of any size and complexity.

See here for detailed tips on real-world usage.

Funding

You can help support continued work on this project, thank you!! ๐Ÿ™

License

Copyright ยฉ 2023-2024 Peter Taoussanis.
Licensed under EPL 1.0 (same as Clojure).

telemere's People

Contributors

ptaoussanis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

madis malesch

telemere's Issues

Add key differences between telemere and mulog to doc section

Hello. First of all thx for the great tool.

I haven't had the opportunity to look into it in detail yet, but it all looks very interesting. Also, I couldnโ€™t help but notice some parallels with mulog and Iโ€™m interested what key differences between telemere and mulog from your point of view. One of the things I was able to notice is clojure support, but i will be glad to read about another points.

Telemere v1.0.0-beta1

  • Core / fundamentals
  • SLF4J integration
  • Timbre migration tools
  • Initial handlers
  • Docstrings, help
  • Wiki docs
  • Demo video

otlp exporter exception

I'm seeing the following logged when I try to log a trace:

[otel.javaagent 2024-08-19 15:32:05:900 -0400] [BatchSpanProcessor_WorkerThread-1] WARN io.opentelemetry.sdk.trace.export.BatchSpanProcessor - Exporter threw an Exception
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
at io.opentelemetry.exporter.internal.otlp.KeyValueMarshaler.create(KeyValueMarshaler.java:83)
at io.opentelemetry.exporter.internal.otlp.KeyValueMarshaler.access$000(KeyValueMarshaler.java:28)
at io.opentelemetry.exporter.internal.otlp.KeyValueMarshaler$1.accept(KeyValueMarshaler.java:63)
at io.opentelemetry.exporter.internal.otlp.KeyValueMarshaler$1.accept(KeyValueMarshaler.java:58)
at java.base/java.util.HashMap.forEach(HashMap.java:1337)
at io.opentelemetry.sdk.internal.AttributesMap.forEach(AttributesMap.java:88)
at io.opentelemetry.exporter.internal.otlp.KeyValueMarshaler.createForAttributes(KeyValueMarshaler.java:57)
at io.opentelemetry.exporter.internal.otlp.traces.SpanMarshaler.create(SpanMarshaler.java:46)
at io.opentelemetry.exporter.internal.marshal.MarshalerUtil.groupByResourceAndScope(MarshalerUtil.java:68)
at io.opentelemetry.exporter.internal.otlp.traces.ResourceSpansMarshaler.groupByResourceAndScope(ResourceSpansMarshaler.java:96)
at io.opentelemetry.exporter.internal.otlp.traces.ResourceSpansMarshaler.create(ResourceSpansMarshaler.java:37)
at io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler.create(TraceRequestMarshaler.java:32)
at io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter.export(OtlpHttpSpanExporter.java:101)
at io.opentelemetry.sdk.trace.export.BatchSpanProcessor$Worker.exportCurrentBatch(BatchSpanProcessor.java:340)
at io.opentelemetry.sdk.trace.export.BatchSpanProcessor$Worker.run(BatchSpanProcessor.java:258)
at java.base/java.lang.Thread.run(Thread.java:829)

I have my project configured to use the otel java agent and to export traces as OTLP. Happy to share more details! Hacking open_telemetry.clj to coerce signal lines to longs seems to remedy it:

(let [ak-uid  (io.opentelemetry.api.common.AttributeKey/stringKey "uid")
      ak-ns   (io.opentelemetry.api.common.AttributeKey/stringKey "ns")
      ak-line (io.opentelemetry.api.common.AttributeKey/longKey   "line")]

  (defn- span-attrs
    "Returns `io.opentelemetry.api.common.Attributes` or nil."
    [uid signal]
    (prn "line" (type (get signal :line)))
    (if uid
      (if-let   [ns   (get signal :ns)]
        (if-let [line (get signal :line)]
          (Attributes/of ak-uid (str uid), ak-ns ns, ak-line (long line))
          (Attributes/of ak-uid (str uid), ak-ns ns))
        (Attributes/of   ak-uid (str uid)))

      (if-let   [ns   (get signal :ns)]
        (if-let [line (get signal :line)]
          (Attributes/of ak-ns ns, ak-line (long line))
          (Attributes/of ak-ns ns))
        nil))))

Failed to load class "org.slf4j.impl.StaticLoggerBinder"

Sorry if this question belies my complete lack of a mental model of what is going on โ€” it's because I have a complete lack of a mental model of what is going on.

When using some Java libraries in Clojure, you occasionally get the startup warning:

Failed to load class "org.slf4j.impl.StaticLoggerBinder"

The standard advice for this with most people I know has been โ€” add slf4j-timbre to your deps.edn, and this warning will magically go away. Since almost all serious projects were using timbre anyway, this seemed like a no-brainer. Somehow somewhere including this library presumably meant that we were creating a binder for SLF4J such that it all goes through to timbre, and then we only need to worry about where that goes.

Now telemere is the better library (OpenTelemetry!) , and I'm starting a new project using it, and I'm getting the dreaded StaticLoggerBinder warning. I saw that you have an SLF4J backend for telemere, which I've included in my requires, but I'm still seeing this logger. I guess I'm doing something wrong.

Make configuring filtering via system vals easier

The current system is very flexible, but also intimidating - especially for beginners.

  • Document this better, including templates/examples
  • Maybe add a util to help serialize current config to env/prop/res?

Disable "host" line printing?

Is there a way to disable printing :host data with every log line? Currently I'm doing the following, which feels kinda hacky:

(taoensso.telemere/handler:file
 {:output-fn (let [f (taoensso.telemere.utils/format-signal-fn)]
               (fn [signal]
                 (f (dissoc signal :host))))})

Compilation error in Shadow-CLJS when Telemere used both in CLJ / CLJS

Hi @ptaoussanis - I'm trying out Telemere in that same app where I'm running one JVM that boots up both a JVM app and a Shadow compiler. I'm getting this error when trying to build the Shadow app:

[server.log] [:browser-dev] Build failure:
[server.log] ------ ERROR -------------------------------------------------------------------
[server.log]  File: jar:file:/Users/dyang/.m2/repository/com/taoensso/telemere/1.0.0-beta11/telemere-1.0.0-beta11.jar!/taoensso/telemere.cljc:45:1
[server.log] --------------------------------------------------------------------------------
[server.log]   42 |
[server.log]   43 | ;;;; Shared signal API
[server.log]   44 |
[server.log]   45 | (sigs/def-api
[server.log] -------^------------------------------------------------------------------------
[server.log] null
[server.log] set! target must be a field or a symbol naming a var at line 45 taoensso/telemere.cljc
[server.log] --------------------------------------------------------------------------------
[server.log]   46 |   {:purpose  "signal"
[server.log]   47 |    :sf-arity 4
[server.log]   48 |    :ct-sig-filter   impl/ct-sig-filter
[server.log]   49 |    :*rt-sig-filter* impl/*rt-sig-filter*
[server.log] --------------------------------------------------------------------------------
[server.log]
[server.log] 2024-05-14 11:12:27.811 INFO EVENT olorin.localdomain taoensso.telemere.tools-logging(47,3) :taoensso.telemere/clojure.tools.logging->telemere! - Enabling intake: `clojure.tools.logging` -> Telemere
[server.log] 2024-05-14 11:12:27.812 INFO EVENT olorin.localdomain taoensso.telemere.streams(132,10) :taoensso.telemere/streams->telemere! - Enabling intake: standard stream/s -> Telemere
[server.log]    data: #:system{:out? true, :err? true}

I'm guessing it's related to the &env check that later calls a set! in def-filter-api?

OpenTelemetry should convert level to string

I'm running Telemere 1.0.0-beta14 with io.opentelemetry/opentelemetry-exporter-otlp 1.39.0, sending to Alloy 1.1.1 to Loki 3.0.

By default, the message "level" (a symbol in Clojure) ends up as a string like ":error" in Loki, which is recognized as "unknown" level. I think signal->attrs-map should pass on (name level) as "level", so "level" is "error". Then Loki colors the events properly.

Idempotence

I'm assuming fns like add-handler! are idempotent, but it would be nice to have it explicitly in the docs.

No namespace info for tools.logging signals

Hi, thanks for all your work on this! I've tried a few different ways to limit ring.logger output like the following to errors, but none of the below filter the log entries:

2024-05-23T20:13:00.759634816Z INFO LOG me :taoensso.telemere/tools-logging - {:request-method :get, :uri "/health", :server-name "localhost", :ring.logger/type :finish, :status 200, :ring.logger/ms 50}
(t/set-min-level! nil "ring.logger" :error)
(t/set-min-level! nil "ring.logger*" :error)
(t/set-min-level! nil "ring.logger.*" :error)
(t/set-min-level! nil "taoensso.telemere/tools-logging" :error)
(t/set-min-level! nil "taoensso.telemere/tools-logging*" :error)
(t/set-min-level! nil "taoensso.telemere/tools-logging.*" :error)
(t/set-min-level! nil "my.app*" :error)

I have this issue with other libraries too, e.g. (t/set-min-level! nil "org.eclipse.jetty" :error) doesn't filter jetty logs.

(t/set-min-level! :error) works fine. Can you advise me on where I'm going wrong?

Root stacktrace is missing

Dear @ptaoussanis,

Thank you so much for this great library. I would like to report the bug of stacktrace not being reported in the console as of 1.0.0-beta-7.

Here is a minimal reproduction:

(tm/log! {:level :info
               :error (ex-info "Some error message" {})}
               "Internal Server Error")

Output

; 2024-05-04T10:18:20.947134Z INFO LOG Macbook.local backend.core (130,1) - Internal Server Error
;
; <<< error <<<
;   Root: clojure.lang.ExceptionInfo - Some error message
;
; Root stack trace:
;
; >>> error >>>

Truncated output from file handler

Have had a report that the file handler may produce truncated output.

My hunch is that the :needs-stopping? dispatch option isn't being enabled by default for this handler (but should be).

TODO:

  • Check that :needs-stopping? is enabled by default for all relevant handlers
  • Confirm that this addresses the issue with truncated output

OpenTelemetry: error! fails with nil body

Trying to log an exception using (error! ex) fails in setBody

value must not be null
  java.util.Objects/requireNonNull at Objects.java:259
  io.opentelemetry.api.incubator.logs.AnyValueString/create at AnyValueString.java:19
  io.opentelemetry.api.incubator.logs.AnyValue/of at AnyValue.java:37
  io.opentelemetry.sdk.logs.SdkLogRecordBuilder/setBody at SdkLogRecordBuilder.java:91
  io.opentelemetry.sdk.logs.SdkLogRecordBuilder/setBody at SdkLogRecordBuilder.java:24
  taoensso.telemere.open_telemetry$handler_COLON_open_telemetry_logger$a_handler_COLON_open_telemetry_logger__14233/invoke at NO_SOURCE_FILE:228
  taoensso.encore.signals$wrap_handler$handle_signal_BANG___12229$fn__12230/invoke at signals.cljc:730
  taoensso.encore.signals$wrap_handler$handle_signal_BANG___12229/invoke at signals.cljc:699

This can be avoided when .setBody is not called. But perhaps a default body should be constructed from exception.type and exception.message, as e.g. Loki will just display an empty line else.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.