From 2ba23ee7f7206bc4e86db325efc785c0b4f0105a Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 29 Apr 2024 09:09:59 +0200 Subject: [PATCH] [new] Add postal (email) handler --- project.clj | 11 +-- src/taoensso/telemere.cljc | 1 - src/taoensso/telemere/postal.clj | 139 +++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/taoensso/telemere/postal.clj diff --git a/project.clj b/project.clj index 66b78b3..7a21e03 100644 --- a/project.clj +++ b/project.clj @@ -46,11 +46,12 @@ [org.clojure/tools.logging "1.3.0"] [org.slf4j/slf4j-api "2.0.13"] [com.taoensso/slf4j-telemere "1.0.0-beta3"] - ;; [org.slf4j/slf4j-simple "2.0.13"] - ;; [org.slf4j/slf4j-nop "2.0.13"] - [io.opentelemetry/opentelemetry-api "1.37.0"] - [io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.37.0"] - [io.opentelemetry/opentelemetry-exporter-otlp "1.37.0"]] + #_[org.slf4j/slf4j-simple "2.0.13"] + #_[org.slf4j/slf4j-nop "2.0.13"] + [com.draines/postal "2.0.5"] + [io.opentelemetry/opentelemetry-api "1.37.0"] + #_[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.37.0"] + #_[io.opentelemetry/opentelemetry-exporter-otlp "1.37.0"]] :plugins [[lein-pprint "1.3.2"] diff --git a/src/taoensso/telemere.cljc b/src/taoensso/telemere.cljc index 5d60229..4259cfe 100644 --- a/src/taoensso/telemere.cljc +++ b/src/taoensso/telemere.cljc @@ -35,7 +35,6 @@ (enc/assert-min-encore-version [3 105 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) diff --git a/src/taoensso/telemere/postal.clj b/src/taoensso/telemere/postal.clj new file mode 100644 index 0000000..3aababc --- /dev/null +++ b/src/taoensso/telemere/postal.clj @@ -0,0 +1,139 @@ +(ns taoensso.telemere.postal + "Email handler using `postal`, + Ref. ." + (:require + [taoensso.encore :as enc :refer [have have?]] + [taoensso.telemere.utils :as utils] + [postal.core :as postal])) + +(comment + (require '[taoensso.telemere :as tel]) + (remove-ns 'taoensso.telemere.postal) + (:api (enc/interns-overview))) + +;;;; Implementation + +(defn format-signal->subject-fn + "Experimental, subject to change. + Returns a (fn format [signal]) that: + - Takes a Telemere signal. + - Returns a formatted email subject like: + \"INFO EVENT :taoensso.telemere.postal/ev-id1 - msg\"" + ([] (format-signal->subject-fn nil)) + ([{:keys [max-len subject-signal-key] + :or + {max-len 128 + subject-signal-key :postal/subject}}] + + (fn format-signal->subject [signal] + (or + (get signal subject-signal-key) ; Custom subject + + ;; Simplified `format-signal->prelude-fn` + (let [{:keys [level kind #_ns id msg_]} signal + sb (enc/str-builder) + s+spc (enc/sb-appender sb " ")] + + (when level (s+spc (utils/format-level level))) + (when kind (s+spc (utils/upper-qn kind))) + (when id (s+spc (utils/format-id nil id))) + (when-let [msg (force msg_)] (s+spc "- " msg)) + + (enc/get-substr-by-len (str sb) 0 max-len)))))) + +(comment + ((format-signal->subject-fn) + (tel/with-signal (tel/event! ::ev-id1 #_{:postal/subject "My subject"})))) + +;;;; Handler + +(defn handler:postal + "Experimental, subject to change. Feedback welcome! + + Needs `postal`, + Ref. . + + Returns a (fn handler [signal]) that: + - Takes a Telemere signal. + - Sends an email with formatted signal content to the configured recipient. + + Useful for emailing important alerts to admins, etc. + + NB can incur financial costs!! + See tips section re: protecting against unexpected costs. + + Options: + + `:postal/conn-opts` - Map of connection opts provided to `postal` + 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.......\"} + + `:postal/msg-opts` - Map of message options + Examples: + {:from \"foo@example.com\", :to \"bar@example.com\"}, + {:from \"Alice \"}, + {:from \"no-reply@example.com\", :to [\"first-responders@example.com\", + \"devops@example.com\"], + :cc \"engineering@example.com\" + :X-MyHeader \"A custom header\"} + + `:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters` + `:format-signal->subject-fn` - (fn [signal]) => email subject string + + Tips: + + - Sending emails can incur financial costs! + Use appropriate dispatch filtering options when calling `add-handler!` to prevent + handler from sending unnecessary emails! + + At least ALWAYS set an appropriate `:rate-limit` option, e.g.: + (add-handler! :my-postal-handler (handler:postal {}) + {:async {:mode :dropping, :buffer-size 128, :n-threads 4} ...}), etc. + + - Ref. for more info on `postal` options." + + ([] (handler:postal nil)) + ([{:keys + [postal/conn-opts + postal/msg-opts + format-signal-fn + format-signal->subject-fn] + + :or + {format-signal-fn (utils/format-signal->str-fn) + format-signal->subject-fn (format-signal->subject-fn)}}] + + (when-not conn-opts (throw (ex-info "No `:postal/conn-opts` was provided" {}))) + (when-not msg-opts (throw (ex-info "No `:postal/msg-opts` was provided" {}))) + + (let [] + (defn a-handler:postal + ([]) ; Shut down (no-op) + ([signal] + (let [msg + (assoc msg-opts + :subject (format-signal->subject-fn) + :body + [{:type "text/plain; charset=utf-8" + :content (format-signal-fn signal)}]) + + [result ex] + (try + [(postal/send-message conn-opts msg) nil] + (catch Exception ex [nil ex])) + + success? (= (get result :code) 0)] + + (when-not success? + (throw (ex-info "Failed to send email" result ex)))))))))