How to get Structured Logging on GCP with Telemere

Felix Barbalet
2024-10-12T22:22:08Z

telemere-gcp-structured-logs.clj

| (ns psithur.data-studio.cloud-run.start
| (:require
| [clojure.stacktrace :as st]
| [clojure.string]
| [jsonista.core :as jsonista]
| [taoensso.telemere :as tele]
| [taoensso.telemere.utils :as utils]))
|
| ; Make sure interop is working
| (assert
| (= (get (tele/check-interop) :slf4j)
| {:present? true
| :telemere-provider-present? true
| :sending->telemere? true
| :telemere-receiving? true}))
|
| (def signal->clean (tele/clean-signal-fn {:incl-kvs? true}))
| ; Output a JSON log, and if it's an error, include the stack-trace so that it gets
| ; picked up by Google Cloud Platform ErrorReporting
| (defn gcp-json-handler
| ([])
| ([{:keys [error]
| :as signal}]
| (let [cleaned (signal->clean signal)]
| (println
| (jsonista.core/write-value-as-string
| (if (utils/error-signal? signal)
| (assoc cleaned :stack_trace (with-out-str (st/print-stack-trace error)))
| cleaned))))))
|
| (def encore-levels->gcp-severity
| {:trace :DEFAULT ; (0) The log entry has no assigned severity level.
| :debug :DEBUG ; (100) Debug or trace information.
| :info :INFO ; (200) Routine information, such as ongoing status or performance.
| :warn :WARNING ; (400) Warning events might cause problems.
| :error :ERROR ; (500) Error events are likely to cause problems.
| :fatal :CRITICAL ; (600) Critical events cause more severe problems or outages.
| :report :ALERT}) ; (700) A person must take an action immediately.
|
| (defn setup-logging
| [profile]
| (when (= :production profile)
| (tele/streams->telemere!)
| (tele/add-handler!
| :default/console
| gcp-json-handler
| {:ns-filter {:disallow "org.eclipse.jetty.server.*"}
| :middleware (fn [{:as signal
| :keys [level ctx]}]
| (let [{:keys [::trace-id ::parent-id ::project]} ctx]
| (cond-> (assoc signal :severity (get encore-levels->gcp-severity level))
| ; Put the trace ID in the right key for GCP Logging to group all signals for this request together
| (some? trace-id)
| (assoc "logging.googleapis.com/trace" (str "projects/" project "/traces/" trace-id))
| ; Include ParentID in the GCP Logging key
| (some? parent-id) (assoc "logging.googleapis.com/spanId" parent-id)
|
| true (dissoc :ctx))))})))
|
| (defn get-context
| [project req]
| (when-let [tp (get-in req [:headers "traceparent"])]
| (let [[version trace-id parent-id trace-flags] (clojure.string/split tp #"\-")]
| {::version version
| ::project project
| ::trace-id trace-id
| ::parent-id parent-id
| ::trace-flags trace-flags})))
| ; Using Reitit-middleware to capture the incoming Trace ID that is saved to the telemere context to be later
| ; appended to the logged signals
| (def ctx-middleware
| {:name ::request-ctx
| :compile (fn [data _]
| (let [project (get-in data [:defaults :project])]
| (fn [handler]
| (fn [request]
| (let [context (get-context project request)]
| (tele/with-ctx+ context (handler request)))))))})