Skip to content

Commit 882569f

Browse files
Support Sentry structured logging (#74)
* Support logging with Sentry * Add support setBeforeSend for log events * Add all main logging function * Add generic log function * Reimplement regular logging functions without macro * Add tests for log specific functions * Add test for log-with-level * Add tests for generic log function * Format code
1 parent 21c6b0d commit 882569f

File tree

4 files changed

+390
-0
lines changed

4 files changed

+390
-0
lines changed

src/sentry_clj/core.clj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
:enable-uncaught-exception-handler true ;; Java SDK default
157157
:trace-options-requests true ;; Java SDK default
158158
:serialization-max-depth 5 ;; default to 5, adjust lower if a circular reference loop occurs.
159+
:logs-enabled false
159160
:enabled true})
160161

161162
(defn ^:private sentry-options
@@ -183,6 +184,8 @@
183184
trace-options-requests
184185
instrumenter
185186
event-processors
187+
logs-enabled
188+
before-send-log-fn
186189
enabled]} (merge sentry-defaults config)
187190
sentry-options (SentryOptions.)]
188191

@@ -241,6 +244,13 @@
241244
.getCustomSamplingContext
242245
.getData)
243246
:transaction-context (.getTransactionContext ctx)})))))
247+
(when logs-enabled
248+
(-> sentry-options .getLogs (.setEnabled true)))
249+
(when before-send-log-fn
250+
(-> sentry-options .getLogs (.setBeforeSend
251+
(reify io.sentry.SentryOptions$Logs$BeforeSendLogCallback
252+
(execute [_ event]
253+
(before-send-log-fn event))))))
244254
(when-let [instrumenter (case instrumenter
245255
:sentry Instrumenter/SENTRY
246256
:otel Instrumenter/OTEL
@@ -292,6 +302,8 @@
292302
| | [More Information)(https://docs.sentry.io/platforms/java/enriching-events/context/) |
293303
| `:traces-sample-rate` | Set a uniform sample rate(a number of between 0.0 and 1.0) for all transactions for tracing |
294304
| `:traces-sample-fn` | A function (taking a custom sample context and a transaction context) enables you to control trace transactions |
305+
| `:logs-enabled` | Enable Sentry structured logging integration | false
306+
| `:before-send-log-fn` | A function (taking a log event) to filter logs, or update them before they are sent to Sentry |
295307
| `:serialization-max-depth` | Set to a lower number, i.e., 2, if you experience circular reference errors when sending events | 5
296308
| `:trace-options-request` | Set to enable or disable tracing of options requests | true
297309
@@ -316,6 +328,10 @@
316328
```clojure
317329
(init! \"http://abcdefg@localhost:19000/2\" {:contexts {:foo \"bar\" :baz \"wibble\"}})
318330
```
331+
332+
```clojure
333+
(init! \"http://abcdefg@localhost:19000/2\" {:logs-enabled true :before-send-log-fn (fn [logEvent] (.setBody logEvent \"new message body\") logEvent)})
334+
```
319335
"
320336
([dsn] (init! dsn {}))
321337
([dsn {:keys [contexts] :as config}]

src/sentry_clj/logging.clj

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
(ns sentry-clj.logging
2+
"Structured logging integration with Sentry.
3+
4+
Provides logging functions at all standard levels:
5+
- `trace`, `debug`, `info`, `warn`, `error`, `fatal` - Level-specific logging functions
6+
- `log` - Generic logging function accepting level as parameter, supports structured logging with maps
7+
8+
## Basic Usage
9+
```clojure
10+
(info \"User logged in: %s\" username)
11+
(error \"Database error: %s\" (.getMessage ex))
12+
```
13+
14+
## Generic Logging
15+
```clojure
16+
(log :warn \"Service unavailable\")
17+
(log :error \"Failed operation: %s\" operation-name)
18+
```
19+
20+
## Structured Logging
21+
```clojure
22+
(log :fatal
23+
{:user-id \"123\" :operation \"payment\" :critical true}
24+
\"Critical system failure\")
25+
```"
26+
(:import [io.sentry Sentry SentryAttributes SentryLogLevel SentryDate SentryAttribute]
27+
[io.sentry.logger SentryLogParameters]))
28+
29+
(defn- get-sentry-logger []
30+
(Sentry/logger))
31+
32+
(defn- log-with-level
33+
"Log a message at the specified level with optional format arguments."
34+
[level message args]
35+
(let [array-params (when (seq args)
36+
(into-array Object args))
37+
logger (get-sentry-logger)]
38+
(case level
39+
:trace (.trace logger message array-params)
40+
:debug (.debug logger message array-params)
41+
:info (.info logger message array-params)
42+
:warn (.warn logger message array-params)
43+
:error (.error logger message array-params)
44+
:fatal (.fatal logger message array-params)
45+
(throw (IllegalArgumentException. (str "Unknown log level: " level))))))
46+
47+
; Convenience functions that delegate to log!
48+
(defn trace [message & args] (log-with-level :trace message args))
49+
(defn debug [message & args] (log-with-level :debug message args))
50+
(defn info [message & args] (log-with-level :info message args))
51+
(defn warn [message & args] (log-with-level :warn message args))
52+
(defn error [message & args] (log-with-level :error message args))
53+
(defn fatal [message & args] (log-with-level :fatal message args))
54+
55+
(defn- keyword->sentry-level
56+
"Converts keyword to SentryLogLevel enum."
57+
[level]
58+
(case level
59+
:trace SentryLogLevel/TRACE
60+
:debug SentryLogLevel/DEBUG
61+
:info SentryLogLevel/INFO
62+
:warn SentryLogLevel/WARN
63+
:error SentryLogLevel/ERROR
64+
:fatal SentryLogLevel/FATAL
65+
(if (instance? SentryLogLevel level)
66+
level
67+
(throw (IllegalArgumentException. (str "Unknown log level: " level))))))
68+
69+
(defn- log-parameters
70+
"Creates SentryLogParameters from a map of attributes.
71+
72+
Automatically detects attribute types and creates appropriate SentryAttribute instances:
73+
- String values -> stringAttribute
74+
- Boolean values -> booleanAttribute
75+
- Integer values -> integerAttribute
76+
- Double/Float values -> doubleAttribute
77+
- Other values -> named attribute
78+
79+
## Example:
80+
```clojure
81+
(log-parameters {:user-id \"123\"
82+
:active true
83+
:count 42
84+
:score 98.5
85+
:metadata {:key \"value\"}})
86+
```"
87+
[attrs-map]
88+
(let [attributes (reduce-kv
89+
(fn [acc k v]
90+
(let [attr-name (name k)
91+
attr (cond
92+
(string? v) (SentryAttribute/stringAttribute attr-name v)
93+
(boolean? v) (SentryAttribute/booleanAttribute attr-name v)
94+
(integer? v) (SentryAttribute/integerAttribute attr-name (long v))
95+
(or (double? v) (float? v)) (SentryAttribute/doubleAttribute attr-name (double v))
96+
:else (SentryAttribute/named attr-name v))]
97+
(conj acc attr)))
98+
[]
99+
attrs-map)]
100+
(SentryLogParameters/create
101+
(SentryAttributes/of (into-array SentryAttribute attributes)))))
102+
103+
(defn log
104+
"Generic logging function that accepts log level and optional parameters.
105+
106+
## Usage Examples
107+
108+
### Basic logging with level keyword:
109+
```clojure
110+
(log :error \"Something went wrong\")
111+
(log :info \"User %s logged in from %s\" username ip-address)
112+
```
113+
114+
### Structured logging with attributes:
115+
```clojure
116+
(log :fatal
117+
{:user-id \"123\"
118+
:operation \"checkout\"
119+
:critical true
120+
:amount 99.99}
121+
\"Payment processing failed for user %s\"
122+
user-id)
123+
```
124+
125+
### Logging with custom timestamp:
126+
```clojure
127+
(log :warn
128+
(SentryInstantDate.)
129+
\"Delayed processing detected at %s\"
130+
(System/currentTimeMillis))
131+
```
132+
133+
## Parameters
134+
- `level` - Log level keyword (`:trace`, `:debug`, `:info`, `:warn`, `:error`, `:fatal`) or SentryLogLevel enum
135+
- `args` - Message and optional parameters: either `[message & format-args]` or `[date-or-params message & format-args]`"
136+
[level & args]
137+
(let [sentry-level (keyword->sentry-level level)
138+
[first-arg second-arg & rest-args] args]
139+
(cond
140+
; Basic case: (log :info "message" arg1 arg2)
141+
(and first-arg (string? first-arg))
142+
(let [message-params (drop 1 args)
143+
array-params (when (seq message-params) (into-array Object message-params))]
144+
(.log (get-sentry-logger) sentry-level first-arg array-params))
145+
146+
; Structured case: (log :info {:attr "val"} "message" arg1 arg2)
147+
(and first-arg second-arg
148+
(or (map? first-arg)
149+
(instance? SentryDate first-arg)
150+
(instance? SentryLogParameters first-arg)))
151+
(let [array-params (when (seq rest-args) (into-array Object rest-args))
152+
logger (get-sentry-logger)]
153+
(cond
154+
(instance? SentryDate first-arg)
155+
(.log logger sentry-level ^SentryDate first-arg second-arg array-params)
156+
157+
(instance? SentryLogParameters first-arg)
158+
(.log logger sentry-level ^SentryLogParameters first-arg second-arg array-params)
159+
160+
(map? first-arg)
161+
(.log logger sentry-level ^SentryLogParameters (log-parameters first-arg) second-arg array-params)))
162+
163+
:else
164+
(throw (IllegalArgumentException.
165+
"Invalid arguments: expected [message & args] or [date-or-params message & args]")))))

test/sentry_clj/core_test.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@
317317
:debug true
318318
:enable-uncaught-exception-handler false
319319
:trace-options-requests false
320+
:logs-enabled true
321+
:before-send-log-fn (fn [event] (.setBody event "new message body") event)
320322
:instrumenter :otel
321323
:event-processors [(SomeEventProcessor.)]
322324
:enabled false})]
@@ -335,6 +337,8 @@
335337
(expect (.isDebug sentry-options))
336338
(expect false (.isEnableUncaughtExceptionHandler sentry-options))
337339
(expect false (.isTraceOptionsRequests sentry-options))
340+
(expect true (.isEnabled (.getLogs sentry-options)))
341+
(expect false (nil? (.getBeforeSend (.getLogs sentry-options))))
338342
(expect Instrumenter/OTEL (.getInstrumenter sentry-options))
339343
(expect (instance? SomeEventProcessor (last (.getEventProcessors sentry-options))))
340344
(expect false (.isEnabled sentry-options)))))

0 commit comments

Comments
 (0)