From 78c307b47421ad939bf01edf5567c9f0d862331e Mon Sep 17 00:00:00 2001 From: David Harrigan Date: Sat, 2 Apr 2022 10:47:05 +0100 Subject: [PATCH] Add Sentry tracing and supporting ring middleware. Big thanks to @karuta0825 for the tracing functionality! Update Sentry Java SDK to 5.7.1. Various library updates. Fixes #32. -=david=- --- .github/workflows/test.yml | 4 +- CHANGELOG.md | 15 +- README.md | 2 + deps.edn | 5 +- examples/README.adoc | 4 + examples/basic/deps.edn | 4 +- examples/basic/src/basic/main.clj | 7 +- examples/ring_with_tracing/deps.edn | 20 +++ .../ring_with_tracing/resources/logback.xml | 20 +++ .../src/ring_with_tracing/main.clj | 50 ++++++ examples/ring_with_tracing/src/user.clj | 7 + examples/uncaught/deps.edn | 4 +- examples/uncaught/src/uncaught/main.clj | 10 +- src/sentry_clj/core.clj | 35 ++-- src/sentry_clj/ring.clj | 107 +++++++++++- src/sentry_clj/tracing.clj | 85 ++++++++++ test/sentry_clj/core_test.clj | 28 ++-- test/sentry_clj/ring_test.clj | 24 ++- test/sentry_clj/tracing_test.clj | 158 ++++++++++++++++++ 19 files changed, 543 insertions(+), 46 deletions(-) create mode 100644 examples/ring_with_tracing/deps.edn create mode 100644 examples/ring_with_tracing/resources/logback.xml create mode 100644 examples/ring_with_tracing/src/ring_with_tracing/main.clj create mode 100644 examples/ring_with_tracing/src/user.clj create mode 100644 src/sentry_clj/tracing.clj create mode 100644 test/sentry_clj/tracing_test.clj diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ba2196..d2fadf5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,12 +16,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '8', '17'] + java: ['8', '17'] steps: - uses: actions/checkout@v3 - name: Setup Cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.m2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a49f0..988b2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,18 @@ commits since the beginning of this repository. ### Added ### Removed +## [5.7.172] + +### Added + +- The ability to use tracing with Sentry. Big thanks to @karuta0825. There is an example in the `examples` directory. + +### Changed + +- Update Sentry Java SDK to 5.7.1. +- Various library updates. +- Update github workers cache to v3 + ## [5.7.171] ### Changed @@ -256,7 +268,8 @@ commits since the beginning of this repository. compatible with Sentry 10.0.1 and below. If you wish to use those versions, please continue to use sentry-clj 1.7.30. -[Unreleased]: https://github.com/getsentry/sentry-clj/compare/5.7.171...HEAD +[Unreleased]: https://github.com/getsentry/sentry-clj/compare/5.7.172...HEAD +[5.7.172]: https://github.com/getsentry/sentry-clj/compare/5.7.171...5.7.172 [5.7.171]: https://github.com/getsentry/sentry-clj/compare/5.6.170...5.7.171 [5.6.170]: https://github.com/getsentry/sentry-clj/compare/5.6.169...5.6.170 [5.6.169]: https://github.com/getsentry/sentry-clj/compare/5.6.166...5.6.169 diff --git a/README.md b/README.md index ea85ab0..4f0d025 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ If you want an interpolated message, you need to provide the full map, i.e., | | [More Information](https://docs.sentry.io/platforms/java/enriching-events/breadcrumbs/) | | `:contexts` | A map of key/value pairs to attach to every Event that is sent. | | | [More Information)(https://docs.sentry.io/platforms/java/enriching-events/context/) | +| `:traces-sample-rate` | Set a uniform sample rate(a number of between 0.0 and 1.0) for all transactions for tracing | +| `:traces-sample-fn` | A function (taking a custom sample context and a transaction context) enables you to control trace transactions | Some examples: diff --git a/deps.edn b/deps.edn index 0054556..62bb6f7 100644 --- a/deps.edn +++ b/deps.edn @@ -4,7 +4,7 @@ ;; ;; ;; - io.sentry/sentry {:mvn/version "5.7.0"} + io.sentry/sentry {:mvn/version "5.7.1"} ring/ring-core {:mvn/version "1.9.5"}} :aliases {:build {:deps {io.github.seancorfield/build-clj {:git/tag "v0.8.0" @@ -13,7 +13,7 @@ :test {:extra-paths ["test"] :extra-deps {cheshire/cheshire {:mvn/version "5.10.2"} - com.github.seancorfield/expectations {:mvn/version "2.0.157"} + com.github.seancorfield/expectations {:mvn/version "2.0.160"} lambdaisland/kaocha {:mvn/version "1.64.1010"} lambdaisland/kaocha-cloverage {:mvn/version "1.0.75"} lambdaisland/kaocha-junit-xml {:mvn/version "0.0.76"} @@ -24,6 +24,7 @@ org.slf4j/slf4j-nop {:mvn/version "1.7.36"}} :exec-fn antq.tool/outdated :exec-args {:directory ["."] + :exclude [org.clojure/clojure] :skip ["pom"] :verbose true :upgrade true diff --git a/examples/README.adoc b/examples/README.adoc index 3d449a8..668d2b8 100644 --- a/examples/README.adoc +++ b/examples/README.adoc @@ -33,3 +33,7 @@ as: `bin/run -d DSN` Where `DSN` is your DSN URL. + +== Tracing + +This example shows how to perform tracing with Ring. diff --git a/examples/basic/deps.edn b/examples/basic/deps.edn index d6e0bac..99e6842 100644 --- a/examples/basic/deps.edn +++ b/examples/basic/deps.edn @@ -5,8 +5,8 @@ ;; ;; ch.qos.logback/logback-classic {:mvn/version "1.2.11"} - io.sentry/sentry {:mvn/version "5.7.0"} - io.sentry/sentry-clj {:mvn/version "5.7.171"} + io.sentry/sentry {:mvn/version "5.7.1"} + io.sentry/sentry-clj {:local/root "../../../sentry-clj"} org.clojure/tools.cli {:mvn/version "1.0.206"} org.clojure/tools.logging {:mvn/version "1.2.4"} org.slf4j/jcl-over-slf4j {:mvn/version "1.7.36"} diff --git a/examples/basic/src/basic/main.clj b/examples/basic/src/basic/main.clj index c680048..e465da4 100644 --- a/examples/basic/src/basic/main.clj +++ b/examples/basic/src/basic/main.clj @@ -1,11 +1,10 @@ (ns basic.main - (:require - [clojure.tools.logging :as log] - [sentry-clj.core :as sentry])) + (:require + [clojure.tools.logging :as log] + [sentry-clj.core :as sentry])) (defn ^:private create-sentry-logger "Create a Sentry Logger using the supplied `dsn`. - If no `dsn` is supplied, simply log the `event` to a `logger`." [{:keys [dsn] :as config}] (if dsn diff --git a/examples/ring_with_tracing/deps.edn b/examples/ring_with_tracing/deps.edn new file mode 100644 index 0000000..99680d5 --- /dev/null +++ b/examples/ring_with_tracing/deps.edn @@ -0,0 +1,20 @@ +{:paths ["src" "resources"] + + :deps {org.clojure/clojure {:mvn/version "1.10.3"} + ;; + ;; + ;; + ch.qos.logback/logback-classic {:mvn/version "1.2.11"} + integrant/integrant {:mvn/version "0.8.0"} + integrant/repl {:mvn/version "0.3.2"} + io.sentry/sentry {:mvn/version "5.7.1"} + io.sentry/sentry-clj {:local/root "../../../sentry-clj"} + org.clojure/tools.cli {:mvn/version "1.0.206"} + org.clojure/tools.logging {:mvn/version "1.2.4"} + org.slf4j/jcl-over-slf4j {:mvn/version "1.7.36"} + org.slf4j/jul-to-slf4j {:mvn/version "1.7.36"} + org.slf4j/log4j-over-slf4j {:mvn/version "1.7.36"} + org.slf4j/slf4j-api {:mvn/version "1.7.36"} + ring/ring-core {:mvn/version "1.9.5"} + ring/ring-jetty-adapter {:mvn/version "1.9.5"} + ring/ring-json {:mvn/version "0.5.1"}}} diff --git a/examples/ring_with_tracing/resources/logback.xml b/examples/ring_with_tracing/resources/logback.xml new file mode 100644 index 0000000..a3937ad --- /dev/null +++ b/examples/ring_with_tracing/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + examples.basic + + + + + + [%boldWhite(%d)] --- [%boldBlue(%-5level)][%boldGreen(%-25.25logger{25})] - %msg%n%rEx + + + + + + + + diff --git a/examples/ring_with_tracing/src/ring_with_tracing/main.clj b/examples/ring_with_tracing/src/ring_with_tracing/main.clj new file mode 100644 index 0000000..6fc83b9 --- /dev/null +++ b/examples/ring_with_tracing/src/ring_with_tracing/main.clj @@ -0,0 +1,50 @@ +(ns ring-with-tracing.main + (:require + [integrant.core :as ig] + [ring.adapter.jetty :refer [run-jetty]] + [ring.middleware.json :refer [wrap-json-params wrap-json-response]] + [ring.middleware.keyword-params :refer [wrap-keyword-params]] + [sentry-clj.core :as sentry] + [sentry-clj.ring :refer [wrap-report-exceptions wrap-sentry-tracing]] + [sentry-clj.tracing :refer [with-start-child-span]])) + +(def config + {:adapter/jetty {:port 8080, :handler (ig/ref :handler/router)} + :handler/router {:app (ig/ref :handler/hello)} + :handler/hello {:name "Sentry"}}) + +(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}] + (run-jetty handler (-> opts (dissoc :handler) (assoc :join? false)))) + +(defmethod ig/init-key :handler/hello [_ {:keys [name]}] + (fn [request] + + (with-start-child-span "task" "my-child-operation" + (Thread/sleep 1000)) + + {:status 200 + :headers {"Content-type" "application/json"} + :body (str "Hi " name)})) + +(defmethod ig/init-key :handler/router [_ {:keys [app]}] + ;; set your dsn + (let [dsn "https://abcdefg@localhost:9000/2"] + (sentry/init! dsn {:dsn dsn + :environment "local" + :traces-sample-rate 1.0 + :debug true})) + + (-> app + (wrap-report-exceptions {}) + wrap-sentry-tracing + wrap-keyword-params + wrap-json-params + wrap-json-response)) + +(defmethod ig/halt-key! :adapter/jetty [_ server] + (.stop server)) + +(defn -main + [& _] + (-> config + ig/init)) diff --git a/examples/ring_with_tracing/src/user.clj b/examples/ring_with_tracing/src/user.clj new file mode 100644 index 0000000..8160ab7 --- /dev/null +++ b/examples/ring_with_tracing/src/user.clj @@ -0,0 +1,7 @@ +(ns user + (:require + [integrant.core :as ig] + [integrant.repl :refer [clear go halt prep init reset reset-all]] + [ring-with-tracing.main])) + +(integrant.repl/set-prep! (comp ig/prep (constantly ring-with-tracing.main/config))) diff --git a/examples/uncaught/deps.edn b/examples/uncaught/deps.edn index d6e0bac..99e6842 100644 --- a/examples/uncaught/deps.edn +++ b/examples/uncaught/deps.edn @@ -5,8 +5,8 @@ ;; ;; ch.qos.logback/logback-classic {:mvn/version "1.2.11"} - io.sentry/sentry {:mvn/version "5.7.0"} - io.sentry/sentry-clj {:mvn/version "5.7.171"} + io.sentry/sentry {:mvn/version "5.7.1"} + io.sentry/sentry-clj {:local/root "../../../sentry-clj"} org.clojure/tools.cli {:mvn/version "1.0.206"} org.clojure/tools.logging {:mvn/version "1.2.4"} org.slf4j/jcl-over-slf4j {:mvn/version "1.7.36"} diff --git a/examples/uncaught/src/uncaught/main.clj b/examples/uncaught/src/uncaught/main.clj index 2c615ba..ddf2e18 100644 --- a/examples/uncaught/src/uncaught/main.clj +++ b/examples/uncaught/src/uncaught/main.clj @@ -1,9 +1,9 @@ (ns uncaught.main - (:require - [clojure.tools.cli :refer [parse-opts]] - [clojure.tools.logging :as log] - [sentry-clj.core :as sentry]) - (:gen-class)) + (:require + [clojure.tools.cli :refer [parse-opts]] + [clojure.tools.logging :as log] + [sentry-clj.core :as sentry]) + (:gen-class)) (defn ^:private set-default-exception-handler "Register our own Uncaught Exception Handler using the provided `sentry-client`." diff --git a/src/sentry_clj/core.clj b/src/sentry_clj/core.clj index 5e06bea..f9d7ddf 100644 --- a/src/sentry_clj/core.clj +++ b/src/sentry_clj/core.clj @@ -21,10 +21,10 @@ :fatal SentryLevel/FATAL SentryLevel/INFO)) -(defn ^:private java-util-hashmappify-vals +(defn java-util-hashmappify-vals "Converts an ordinary Clojure map into a Clojure map with nested map - values recursively translated into java.util.HashMap objects. Based - on walk/stringify-keys." + values recursively translated into java.util.HashMap objects. Based + on walk/stringify-keys." [m] (let [f (fn [[k v]] (let [k (if (keyword? k) (name k) k) @@ -157,7 +157,9 @@ enable-uncaught-exception-handler ;; deprecated uncaught-handler-enabled before-send-fn - before-breadcrumb-fn]} (merge sentry-defaults config) + before-breadcrumb-fn + traces-sample-rate + traces-sample-fn]} (merge sentry-defaults config) sentry-options (SentryOptions.)] (.setDsn sentry-options dsn) @@ -196,7 +198,16 @@ (reify io.sentry.SentryOptions$BeforeBreadcrumbCallback (execute [_ breadcrumb hint] (before-breadcrumb-fn breadcrumb hint))))) - + (when traces-sample-rate + (.setTracesSampleRate sentry-options traces-sample-rate)) + (when traces-sample-fn + (.setTracesSampler sentry-options ^io.sentry.SentryOptions$TracesSamplerCallback + (reify io.sentry.SentryOptions$TracesSamplerCallback + (sample [_ ctx] + (traces-sample-fn {:custom-sample-context (-> ctx + .getCustomSamplingContext + .getData) + :transaction-context (.getTransactionContext ctx)}))))) sentry-options)) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} @@ -228,6 +239,8 @@ | | [More Information](https://docs.sentry.io/platforms/java/enriching-events/breadcrumbs/) | | `:contexts` | A map of key/value pairs to attach to every Event that is sent. | | | [More Information)(https://docs.sentry.io/platforms/java/enriching-events/context/) | + | `:traces-sample-rate` | Set a uniform sample rate(a number of between 0.0 and 1.0) for all transactions for tracing | + | `:traces-sample-fn` | A function (taking a custom sample context and a transaction context) enables you to control trace transactions | Some examples: @@ -271,12 +284,12 @@ (defn send-event "Sends the given event to Sentry, returning the event's id - Supports sending throwables: + Supports sending throwables: - ``` - (sentry/send-event {:message \"oh no\", - :throwable (RuntimeException. \"foo bar\"}) - ``` - " + ``` + (sentry/send-event {:message \"oh no\", + :throwable (RuntimeException. \"foo bar\"}) + ``` + " [event] (str (Sentry/captureEvent (map->event event)))) diff --git a/src/sentry_clj/ring.clj b/src/sentry_clj/ring.clj index eab9c6b..adf39fe 100644 --- a/src/sentry_clj/ring.clj +++ b/src/sentry_clj/ring.clj @@ -1,12 +1,20 @@ (ns sentry-clj.ring "Ring utility functions." (:require + [clojure.string :refer [upper-case]] [ring.util.request :refer [request-url]] [ring.util.response :as response] - [sentry-clj.core :as sentry])) + [sentry-clj.core :as sentry] + [sentry-clj.tracing :as st]) + (:import + [io.sentry EventProcessor Hub Sentry SentryEvent SentryTraceHeader] + [io.sentry.protocol SentryTransaction Request])) (set! *warn-on-reflection* true) +(def ^:private sentry-trace-header + SentryTraceHeader/SENTRY_TRACE_HEADER) + (defn ^:private request->http "Converts a Ring request into an HTTP interface for an event." [req] @@ -18,6 +26,20 @@ :env {:session (-> req :session pr-str) "REMOTE_ADDR" (:remote-addr req)}}) +(defn ^:private get-current-hub + "Get current Hub." + [] + (Sentry/getCurrentHub)) + +(defn ^:private configure-scope! + "Set scope a callback function which is called + before a transaction finish or an event is send to Sentry." + [^Hub hub scope-cb] + (.configureScope hub (reify io.sentry.ScopeCallback + (run + [_ scope] + (scope-cb scope))))) + (defn ^:private request->user "Converts a Ring request into a User interface for an event." [req] @@ -39,10 +61,63 @@ (response/content-type "text/html") (response/status 500))) +(defn ^:private extract-transaction-name + "Extract transactin name from request. + ex) GET /api/status" + [{:keys [request-method uri]}] + (str (-> request-method name upper-case) " " uri)) + +(defn ^:private request->context-request + "Converts a request into custom-sampling-context's request." + [req] + {:uri (request-url req) + :query-string (:query-string req "") + :method (-> req :request-method name upper-case) + :headers (:headers req) + :data (-> req :params)}) + +(defn ^:private compute-sentry-runtime + "Compute Clojure runtime information." + [] + (let [runtime (io.sentry.protocol.SentryRuntime.)] + (.setName runtime "Clojure") + (.setVersion runtime (clojure-version)) + runtime)) + +(defn ^:private map->request + "Converts a map into a Request." + [{:keys [uri request-method query-string params headers] :as req}] + (let [request (Request.)] + (when uri + (.setUrl request (request-url req))) + (when request-method + (.setMethod request (-> request-method name upper-case))) + (when query-string + (.setQueryString request query-string)) + (when params + (.setData request (sentry/java-util-hashmappify-vals params))) + (when headers + (.setHeaders request (sentry/java-util-hashmappify-vals headers))) + request)) + +(defn ^:private event-processor + "This process is executed before a transaction finish or an event is sent." + [] + (reify EventProcessor + (^SentryEvent process + [_ ^SentryEvent event _] + (.setRuntime (.getContexts event) (compute-sentry-runtime)) + event) + + (^SentryTransaction process + [_ ^SentryTransaction tran _] + (.setRuntime (.getContexts tran) (compute-sentry-runtime)) + tran))) + (defn wrap-report-exceptions "Wraps the given handler in error reporting. - Optionally takes three functions: + Optionally takes three functions: * `:preprocess-fn`, which is passed the request * `:postprocess-fn`, which is passed the request and the Sentry event @@ -62,3 +137,31 @@ (->> (postprocess-fn req) sentry/send-event)) (error-fn req e))))) + +(defn wrap-sentry-tracing + "Wraps the given handler in tracing" + [handler] + (fn [req] + (let [trace-id (get (:headers req) sentry-trace-header) + name (extract-transaction-name req) + custom-sampling-context (->> req + request->context-request + (st/compute-custom-sampling-context "request")) + transaction (st/start-transaction name + "http.server" + custom-sampling-context + trace-id)] + (-> (get-current-hub) + (configure-scope! (fn [scope] + (st/swap-scope-request! scope (map->request req)) + (st/add-event-processor scope (event-processor))))) + + (try + (let [res (handler req)] + (st/swap-transaction-status! transaction (:ok st/span-status)) + res) + (catch Throwable e + (st/swap-transaction-status! transaction (:internal-error st/span-status)) + (throw e)) + (finally + (st/finish-transaction! transaction)))))) diff --git a/src/sentry_clj/tracing.clj b/src/sentry_clj/tracing.clj new file mode 100644 index 0000000..200dadf --- /dev/null +++ b/src/sentry_clj/tracing.clj @@ -0,0 +1,85 @@ +(ns sentry-clj.tracing + (:import + [io.sentry + CustomSamplingContext + EventProcessor + Scope + Sentry + SentryTracer + SpanStatus + TransactionContext])) + +(def span-status + {:ok SpanStatus/OK + :cancel SpanStatus/CANCELLED + :internal-error SpanStatus/INTERNAL_ERROR + :unknown SpanStatus/UNKNOWN + :unknown-error SpanStatus/UNKNOWN_ERROR + :invalid-argument SpanStatus/INVALID_ARGUMENT + :deadline-exceeded SpanStatus/DEADLINE_EXCEEDED + :not-found SpanStatus/NOT_FOUND + :already-exists SpanStatus/ALREADY_EXISTS + :permisson-denied SpanStatus/PERMISSION_DENIED + :resource-exhaused SpanStatus/RESOURCE_EXHAUSTED + :fail-precondition SpanStatus/FAILED_PRECONDITION + :aborted SpanStatus/ABORTED + :out-of-range SpanStatus/OUT_OF_RANGE + :unimplemented SpanStatus/UNIMPLEMENTED + :unavailable SpanStatus/UNAVAILABLE + :data-loss SpanStatus/DATA_LOSS + :unauthenticated SpanStatus/UNAUTHENTICATED}) + +(defn ^CustomSamplingContext compute-custom-sampling-context + "Compute a custom sampling context has key and info." + [key info] + (let [csc (CustomSamplingContext.)] + (.set csc key info) + csc)) + +(defn start-transaction + "Start tracing transactions. + If a sentry-trace-header is given, connect the exsiting transaction." + [name operation custom-sampling-context sentry-trace-header] + (if sentry-trace-header + (let [contexts (TransactionContext/fromSentryTrace name operation (io.sentry.SentryTraceHeader. sentry-trace-header))] + (-> (Sentry/getCurrentHub) + (.startTransaction contexts ^CustomSamplingContext custom-sampling-context true))) + (-> (Sentry/getCurrentHub) + (.startTransaction ^String name "http.server" ^CustomSamplingContext custom-sampling-context true)))) + +(defn swap-scope-request! + "Set request info to the scope." + [^Scope scope req] + (.setRequest scope req)) + +(defn add-event-processor + "Add Event Processor to the scope. + event-processor is executed when tracing transaction finish or capture error event." + [^Scope scope ^EventProcessor event-processor] + (.addEventProcessor scope event-processor)) + +(defn swap-transaction-status! + "Set trace transaction status." + [^SentryTracer transaction status] + (.setStatus transaction status)) + +(defn finish-transaction! + "Finish trace transaction and send event to Sentry." + [^SentryTracer transaction] + (.finish transaction)) + +(defmacro with-start-child-span + "Start a child span which has the operation or description + and finish after evaluating forms." + [operation description & forms] + `(when-let [sp# (Sentry/getSpan)] + (let [inner-sp# (.startChild sp# ~operation ~description)] + (try + ~@forms + (.setStatus inner-sp# SpanStatus/OK) + (catch Throwable e# + (.setThrowable inner-sp# e#) + (.setStatus inner-sp# SpanStatus/INTERNAL_ERROR) + (throw e#)) + (finally + (.finish inner-sp#)))))) diff --git a/test/sentry_clj/core_test.clj b/test/sentry_clj/core_test.clj index 595f07e..a72d801 100644 --- a/test/sentry_clj/core_test.clj +++ b/test/sentry_clj/core_test.clj @@ -302,17 +302,17 @@ :in-app-excludes ["com.excludes" "com.excludes2"] :ignored-exceptions-for-type ["java.io.IOException" "java.lang.RuntimeException"] :uncaught-handler-enabled true})] - (expect "http://www.example.com" (.getDsn sentry-options)) - (expect "production" (.getEnvironment sentry-options)) - (expect (.isDebug sentry-options)) - (expect "1.1" (.getRelease sentry-options)) - (expect "x86" (.getDist sentry-options)) - (expect "host1" (.getServerName sentry-options)) - (expect 1000 (.getShutdownTimeout sentry-options)) - (expect "com.includes" (first (.getInAppIncludes sentry-options))) - (expect "com.includes2" (second (.getInAppIncludes sentry-options))) - (expect "com.excludes" (first (.getInAppExcludes sentry-options))) - (expect "com.excludes2" (second (.getInAppExcludes sentry-options))) - (expect (isa? (first (.getIgnoredExceptionsForType sentry-options)) java.io.IOException)) - (expect (isa? (second (.getIgnoredExceptionsForType sentry-options)) java.lang.RuntimeException)) - (expect true (.isEnableUncaughtExceptionHandler sentry-options))))) + (expect "http://www.example.com" (.getDsn sentry-options)) + (expect "production" (.getEnvironment sentry-options)) + (expect (.isDebug sentry-options)) + (expect "1.1" (.getRelease sentry-options)) + (expect "x86" (.getDist sentry-options)) + (expect "host1" (.getServerName sentry-options)) + (expect 1000 (.getShutdownTimeout sentry-options)) + (expect "com.includes" (first (.getInAppIncludes sentry-options))) + (expect "com.includes2" (second (.getInAppIncludes sentry-options))) + (expect "com.excludes" (first (.getInAppExcludes sentry-options))) + (expect "com.excludes2" (second (.getInAppExcludes sentry-options))) + (expect (isa? (first (.getIgnoredExceptionsForType sentry-options)) java.io.IOException)) + (expect (isa? (second (.getIgnoredExceptionsForType sentry-options)) java.lang.RuntimeException)) + (expect true (.isEnableUncaughtExceptionHandler sentry-options))))) diff --git a/test/sentry_clj/ring_test.clj b/test/sentry_clj/ring_test.clj index 8fc8759..f8ca025 100644 --- a/test/sentry_clj/ring_test.clj +++ b/test/sentry_clj/ring_test.clj @@ -7,7 +7,9 @@ (:import [io.sentry GsonSerializer - SentryOptions] + SentryOptions + Hub + Sentry] [java.io StringWriter])) (defn serialize @@ -112,3 +114,23 @@ "url" "https://example.com/hello-world"}, "sdk" {"version" "blah"}, "user" {}} sentry-event)))) + +(defn- get-test-options + ([] (get-test-options {})) + ([{:keys [traces-sample-rate]}] + (let [sentry-option (SentryOptions.)] + (.setDsn sentry-option "https://key@sentry.io/proj") + (.setEnvironment sentry-option "development") + (.setRelease sentry-option "release@1.0.0") + (when traces-sample-rate + (.setTracesSampleRate sentry-option traces-sample-rate)) + sentry-option))) + +(defexpect wrap-sentry-tracing-test + (expecting + "passing through" + (let [sentry-option (get-test-options {:traces-sample-rate 1.0 :debug true}) + hub (Hub. sentry-option) + handler (ring/wrap-sentry-tracing wrapped)] + (Sentry/setCurrentHub hub) + (expect "woo" (handler (assoc req :ok true)))))) diff --git a/test/sentry_clj/tracing_test.clj b/test/sentry_clj/tracing_test.clj new file mode 100644 index 0000000..b7b16a7 --- /dev/null +++ b/test/sentry_clj/tracing_test.clj @@ -0,0 +1,158 @@ +(ns sentry-clj.tracing-test + (:require + [expectations.clojure.test :refer [defexpect expect expecting]] + [sentry-clj.tracing :as sut]) + (:import + [io.sentry + CustomSamplingContext + Hub + Scope + Sentry + SentryOptions + SentryTracer + Span + SpanStatus + TransactionContext] + [io.sentry.protocol + Request + SentryId])) + +(defn- get-test-options + ([] (get-test-options {})) + ([{:keys [traces-sample-rate]}] + (let [sentry-option (SentryOptions.)] + (.setDsn sentry-option "https://key@sentry.io/proj") + (.setEnvironment sentry-option "development") + (.setRelease sentry-option "release@1.0.0") + (when traces-sample-rate + (.setTracesSampleRate sentry-option traces-sample-rate)) + sentry-option))) + +(defn- ^SentryTracer get-test-sentry-tracer + [] + (let [sentry-option (get-test-options) + hub (Hub. sentry-option) + tr (SentryTracer. (TransactionContext. "name" "op" true) hub)] + (Sentry/setCurrentHub hub) + (.configureScope hub (reify io.sentry.ScopeCallback + (run + [_ scope] + (.setTransaction scope tr)))) + tr)) + +(defexpect span-status-test + (expecting + "extract span status by keyword" + (expect SpanStatus/OK (:ok sut/span-status)) + (expect SpanStatus/CANCELLED (:cancel sut/span-status)) + (expect SpanStatus/INTERNAL_ERROR (:internal-error sut/span-status)) + (expect SpanStatus/UNKNOWN (:unknown sut/span-status)) + (expect SpanStatus/UNKNOWN_ERROR (:unknown-error sut/span-status)) + (expect SpanStatus/INVALID_ARGUMENT (:invalid-argument sut/span-status)) + (expect SpanStatus/NOT_FOUND (:not-found sut/span-status)) + (expect SpanStatus/ALREADY_EXISTS (:already-exists sut/span-status)) + (expect SpanStatus/PERMISSION_DENIED (:permisson-denied sut/span-status)) + (expect SpanStatus/RESOURCE_EXHAUSTED (:resource-exhaused sut/span-status)) + (expect SpanStatus/FAILED_PRECONDITION (:fail-precondition sut/span-status)) + (expect SpanStatus/ABORTED (:aborted sut/span-status)) + (expect SpanStatus/OUT_OF_RANGE (:out-of-range sut/span-status)) + (expect SpanStatus/UNIMPLEMENTED (:unimplemented sut/span-status)) + (expect SpanStatus/DATA_LOSS (:data-loss sut/span-status)) + (expect SpanStatus/UNAUTHENTICATED (:unauthenticated sut/span-status)))) + +(defexpect compute-custom-sampling-context-test + (expecting + "compute custom-sampling-context" + (let [request {"url" "http://example.com" + "method" "GET" + "headers" {"X-Clacks-Overhead" "Terry Pratchett" + "X-w00t" "ftw!"} + "data" "data"} + csc (sut/compute-custom-sampling-context "request" request)] + (expect (.get csc "request") request)))) + +(defexpect swap-transaction-status-test + (expecting + "change transaction status" + (let [tr (get-test-sentry-tracer)] + (sut/swap-transaction-status! tr (:ok sut/span-status)) + (expect (.getStatus tr) (:ok sut/span-status))))) + +(defexpect finish-transaction!-test + (expecting + "transaction is finished" + (let [tr (get-test-sentry-tracer)] + (sut/finish-transaction! tr) + (expect (.isFinished tr) true)))) + +(defexpect swap-scope-request! + (expecting + "set scope Request information" + (let [option (get-test-options) + scope (Scope. option) + request (Request.) + url "http://example.com" + method "GET" + query-string "?foo=bar" + data {"baz" 1}] + (.setUrl request url) + (.setMethod request method) + (.setQueryString request query-string) + (.setData request data) + (sut/swap-scope-request! scope request) + + (expect (-> (.getRequest scope) .getUrl) url) + (expect (-> (.getRequest scope) .getMethod) method) + (expect (-> (.getRequest scope) .getQueryString) query-string) + (expect (-> (.getRequest scope) .getData) data)))) + +(defexpect with-start-child-span-test + (expecting + "when a child span is started and works correctly, span status is OK" + (let [tr (get-test-sentry-tracer)] + (sut/with-start-child-span "op" "desc" (println "hi")) + (expect (.getStatus ^Span (nth (.getChildren tr) 0)) (:ok sut/span-status)) + (sut/finish-transaction! tr))) + (expecting + "when a child span is started and throw exceptions, span status is INTERNAL_ERROR" + (let [tr (get-test-sentry-tracer)] + (try + (sut/with-start-child-span "op" "desc" (throw (ex-info "something-error" {}))) + (catch Throwable _) + (finally + (expect (.getStatus ^Span (nth (.getChildren tr) 0)) (:internal-error sut/span-status)) + (sut/finish-transaction! tr)))))) + +(defexpect start-transaction-test + (expecting + "when trace option isn't set, trace transaction isn't created" + (let [sentry-option (get-test-options) + hub (Hub. sentry-option)] + (Sentry/setCurrentHub hub) + (let [tr (sut/start-transaction "op" "http.server" (CustomSamplingContext.) nil)] + (expect io.sentry.NoOpTransaction tr)))) + (expecting + "when there is not a sentry-trace-header, new trace transaction is created" + (let [sentry-option (get-test-options {:traces-sample-rate 1.0}) + hub (Hub. sentry-option) + operation "opration" + description "http.server"] + (Sentry/setCurrentHub hub) + (let [^SentryTracer tr (sut/start-transaction operation description (CustomSamplingContext.) nil)] + (expect (.getName tr) operation) + (expect (.getOperation tr) description) + (expect (complement nil?) (.getTraceId (.toSentryTrace tr))) + (sut/finish-transaction! tr)))) + (expecting + "when there is a sentry-trace-header, exist trace transaction is gotten" + (let [sentry-option (get-test-options {:traces-sample-rate 1.0}) + hub (Hub. sentry-option) + sentry-trace-header "f084f508efca47e9a3313851d4d8b7a2" + operation "opration" + description "http.server"] + (Sentry/setCurrentHub hub) + (let [^SentryTracer tr (sut/start-transaction operation description (CustomSamplingContext.) (str sentry-trace-header "-" "c1"))] + (expect (.getName tr) operation) + (expect (.getOperation tr) description) + (expect (SentryId. sentry-trace-header) (.getTraceId (.toSentryTrace tr))) + (sut/finish-transaction! tr)))))