Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/sentry_clj/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
:enable-uncaught-exception-handler true ;; Java SDK default
:trace-options-requests true ;; Java SDK default
:serialization-max-depth 5 ;; default to 5, adjust lower if a circular reference loop occurs.
:logs-enabled false
:enabled true})

(defn ^:private sentry-options
Expand Down Expand Up @@ -183,6 +184,8 @@
trace-options-requests
instrumenter
event-processors
logs-enabled
before-send-log-fn
enabled]} (merge sentry-defaults config)
sentry-options (SentryOptions.)]

Expand Down Expand Up @@ -241,6 +244,13 @@
.getCustomSamplingContext
.getData)
:transaction-context (.getTransactionContext ctx)})))))
(when logs-enabled
(-> sentry-options .getLogs (.setEnabled true)))
(when before-send-log-fn
(-> sentry-options .getLogs (.setBeforeSend
(reify io.sentry.SentryOptions$Logs$BeforeSendLogCallback
(execute [_ event]
(before-send-log-fn event))))))
(when-let [instrumenter (case instrumenter
:sentry Instrumenter/SENTRY
:otel Instrumenter/OTEL
Expand Down Expand Up @@ -292,6 +302,8 @@
| | [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 |
| `:logs-enabled` | Enable Sentry structured logging integration | false
| `:before-send-log-fn` | A function (taking a log event) to filter logs, or update them before they are sent to Sentry |
| `:serialization-max-depth` | Set to a lower number, i.e., 2, if you experience circular reference errors when sending events | 5
| `:trace-options-request` | Set to enable or disable tracing of options requests | true

Expand All @@ -316,6 +328,10 @@
```clojure
(init! \"http://abcdefg@localhost:19000/2\" {:contexts {:foo \"bar\" :baz \"wibble\"}})
```

```clojure
(init! \"http://abcdefg@localhost:19000/2\" {:logs-enabled true :before-send-log-fn (fn [logEvent] (.setBody logEvent \"new message body\") logEvent)})
```
"
([dsn] (init! dsn {}))
([dsn {:keys [contexts] :as config}]
Expand Down
165 changes: 165 additions & 0 deletions src/sentry_clj/logging.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
(ns sentry-clj.logging
"Structured logging integration with Sentry.

Provides logging functions at all standard levels:
- `trace`, `debug`, `info`, `warn`, `error`, `fatal` - Level-specific logging functions
- `log` - Generic logging function accepting level as parameter, supports structured logging with maps

## Basic Usage
```clojure
(info \"User logged in: %s\" username)
(error \"Database error: %s\" (.getMessage ex))
```

## Generic Logging
```clojure
(log :warn \"Service unavailable\")
(log :error \"Failed operation: %s\" operation-name)
```

## Structured Logging
```clojure
(log :fatal
{:user-id \"123\" :operation \"payment\" :critical true}
\"Critical system failure\")
```"
(:import [io.sentry Sentry SentryAttributes SentryLogLevel SentryDate SentryAttribute]
[io.sentry.logger SentryLogParameters]))

(defn- get-sentry-logger []
(Sentry/logger))

(defn- log-with-level
"Log a message at the specified level with optional format arguments."
[level message args]
(let [array-params (when (seq args)
(into-array Object args))
logger (get-sentry-logger)]
(case level
:trace (.trace logger message array-params)
:debug (.debug logger message array-params)
:info (.info logger message array-params)
:warn (.warn logger message array-params)
:error (.error logger message array-params)
:fatal (.fatal logger message array-params)
(throw (IllegalArgumentException. (str "Unknown log level: " level))))))

; Convenience functions that delegate to log!
(defn trace [message & args] (log-with-level :trace message args))
(defn debug [message & args] (log-with-level :debug message args))
(defn info [message & args] (log-with-level :info message args))
(defn warn [message & args] (log-with-level :warn message args))
(defn error [message & args] (log-with-level :error message args))
(defn fatal [message & args] (log-with-level :fatal message args))

(defn- keyword->sentry-level
"Converts keyword to SentryLogLevel enum."
[level]
(case level
:trace SentryLogLevel/TRACE
:debug SentryLogLevel/DEBUG
:info SentryLogLevel/INFO
:warn SentryLogLevel/WARN
:error SentryLogLevel/ERROR
:fatal SentryLogLevel/FATAL
(if (instance? SentryLogLevel level)
level
(throw (IllegalArgumentException. (str "Unknown log level: " level))))))

(defn- log-parameters
"Creates SentryLogParameters from a map of attributes.

Automatically detects attribute types and creates appropriate SentryAttribute instances:
- String values -> stringAttribute
- Boolean values -> booleanAttribute
- Integer values -> integerAttribute
- Double/Float values -> doubleAttribute
- Other values -> named attribute

## Example:
```clojure
(log-parameters {:user-id \"123\"
:active true
:count 42
:score 98.5
:metadata {:key \"value\"}})
```"
[attrs-map]
(let [attributes (reduce-kv
(fn [acc k v]
(let [attr-name (name k)
attr (cond
(string? v) (SentryAttribute/stringAttribute attr-name v)
(boolean? v) (SentryAttribute/booleanAttribute attr-name v)
(integer? v) (SentryAttribute/integerAttribute attr-name (long v))
(or (double? v) (float? v)) (SentryAttribute/doubleAttribute attr-name (double v))
:else (SentryAttribute/named attr-name v))]
(conj acc attr)))
[]
attrs-map)]
(SentryLogParameters/create
(SentryAttributes/of (into-array SentryAttribute attributes)))))

(defn log
"Generic logging function that accepts log level and optional parameters.

## Usage Examples

### Basic logging with level keyword:
```clojure
(log :error \"Something went wrong\")
(log :info \"User %s logged in from %s\" username ip-address)
```

### Structured logging with attributes:
```clojure
(log :fatal
{:user-id \"123\"
:operation \"checkout\"
:critical true
:amount 99.99}
\"Payment processing failed for user %s\"
user-id)
```

### Logging with custom timestamp:
```clojure
(log :warn
(SentryInstantDate.)
\"Delayed processing detected at %s\"
(System/currentTimeMillis))
```

## Parameters
- `level` - Log level keyword (`:trace`, `:debug`, `:info`, `:warn`, `:error`, `:fatal`) or SentryLogLevel enum
- `args` - Message and optional parameters: either `[message & format-args]` or `[date-or-params message & format-args]`"
[level & args]
(let [sentry-level (keyword->sentry-level level)
[first-arg second-arg & rest-args] args]
(cond
; Basic case: (log :info "message" arg1 arg2)
(and first-arg (string? first-arg))
(let [message-params (drop 1 args)
array-params (when (seq message-params) (into-array Object message-params))]
(.log (get-sentry-logger) sentry-level first-arg array-params))

; Structured case: (log :info {:attr "val"} "message" arg1 arg2)
(and first-arg second-arg
(or (map? first-arg)
(instance? SentryDate first-arg)
(instance? SentryLogParameters first-arg)))
(let [array-params (when (seq rest-args) (into-array Object rest-args))
logger (get-sentry-logger)]
(cond
(instance? SentryDate first-arg)
(.log logger sentry-level ^SentryDate first-arg second-arg array-params)

(instance? SentryLogParameters first-arg)
(.log logger sentry-level ^SentryLogParameters first-arg second-arg array-params)

(map? first-arg)
(.log logger sentry-level ^SentryLogParameters (log-parameters first-arg) second-arg array-params)))

:else
(throw (IllegalArgumentException.
"Invalid arguments: expected [message & args] or [date-or-params message & args]")))))
4 changes: 4 additions & 0 deletions test/sentry_clj/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@
:debug true
:enable-uncaught-exception-handler false
:trace-options-requests false
:logs-enabled true
:before-send-log-fn (fn [event] (.setBody event "new message body") event)
:instrumenter :otel
:event-processors [(SomeEventProcessor.)]
:enabled false})]
Expand All @@ -335,6 +337,8 @@
(expect (.isDebug sentry-options))
(expect false (.isEnableUncaughtExceptionHandler sentry-options))
(expect false (.isTraceOptionsRequests sentry-options))
(expect true (.isEnabled (.getLogs sentry-options)))
(expect false (nil? (.getBeforeSend (.getLogs sentry-options))))
(expect Instrumenter/OTEL (.getInstrumenter sentry-options))
(expect (instance? SomeEventProcessor (last (.getEventProcessors sentry-options))))
(expect false (.isEnabled sentry-options)))))
Expand Down
Loading