# Logging

This guide explains how logging works in ShipClojure, how to configure log levels for different environments, and how to add logging to your own code.

***

## Overview

ShipClojure uses [Telemere](https://github.com/taoensso/telemere) as its logging library. Telemere is the next-generation replacement for Timbre, providing structured telemetry with a unified API.

**Key Features:**

* Structured JSON logging for production
* Contextual logging that accumulates request information
* Unified backend for both Clojure logs and Java/SLF4J logs
* Per-namespace log level configuration
* Automatic sensitive data redaction

***

## Telemere as the Unified Logging Backend

ShipClojure uses Telemere for **all** logging, including logs from Java libraries. This is achieved through `telemere-slf4j`, which routes all SLF4J logging calls through Telemere.

### Dependencies

```clojure
;; In deps.edn
com.taoensso/telemere {:mvn/version "1.2.0"}
com.taoensso/telemere-slf4j {:mvn/version "1.2.0"}
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
```

This setup means:

* Your Clojure code uses `taoensso.telemere` directly
* Java libraries (HikariCP, HTTP-Kit, PostgreSQL driver, etc.) use SLF4J, which routes to Telemere
* All logs go through the same handlers and filters

### Verifying Interop

You can verify the SLF4J integration is working in your REPL:

```clojure
(require '[taoensso.telemere :as t])
(t/check-interop)
;; => {:slf4j {:sending->telemere? true, :telemere-receiving? true}}
```

***

## Basic Logging

### Creating Logs

Use `t/log!` for basic logging:

```clojure
(require '[taoensso.telemere :as t])

;; Simple message
(t/log! :info "User logged in")

;; With data
(t/log! {:level :info
         :id :user/login
         :data {:user-id 123}}
        "User logged in")

;; With error
(t/log! {:level :error
         :error ex}
        "Failed to process payment")
```

### Log Levels

Telemere supports these levels (from most to least verbose):

* `:trace`
* `:debug`
* `:info`
* `:warn`
* `:error`
* `:fatal`

***

## Contextual Logging

One of Telemere's most powerful features is contextual logging with `t/with-ctx+`. ShipClojure uses this to gradually add context as a request flows through the middleware chain.

### How Context Accumulates

```
1. API call starts
   → Add {:request-method :uri :server-name :req-id} to log context

   2. Request params are parsed
      → Add params to log context (with sensitive data redacted)

      3. JWT is decoded to get user claims
         → Add relevant user info to log context

         4. Business logic logs have full request context
```

This means any log statement deep in your business logic automatically includes the request method, URI, request ID, user info, and more.

### Implementation

The context is added through Ring middleware in `saas.web.middleware.logger`:

```clojure
(defn wrap-log-request-start
  "Ring middleware to log basic information about a request.
  Adds the key :saas.logging/start-ms to the request map"
  [handler]
  (fn [request]
    (t/with-xfn reduce-request-log-noise-xf
      (t/with-ctx+ (into {:req-id (uuid)}
                         (select-keys request [:request-method :uri :server-name]))
        (handler (log-request-start request))))))
```

Later middleware adds more context:

```clojure
(defn wrap-log-request-params
  [handler]
  (fn [request]
    (let [params (redacted-request-params request)]
      (t/with-ctx+ params
        (handler request)))))
```

### Benefits

With this setup, every log entry contains the full context:

```json
{
  "timestamp": "2025-10-16T06:30:22.287897Z",
  "level": "info",
  "id": "db/query",
  "msg": "Fetched user profile",
  "ctx": {
    "req-id": "5efd277d-1ee7-46f8-94d7-93131c0d628a",
    "request-method": "post",
    "uri": "/cqrs/query",
    "server-name": "localhost",
    "params": {
      "query/kind": ":query/get-profile"
    }
  }
}
```

You can trace all logs for a single request by filtering on `req-id`.

## Configuring Log Levels

### Per-Environment Configuration

Log levels are configured in the environment-specific `env.clj` files:

* `env/dev/clj/saas/env.clj` - Development settings
* `env/prod/clj/saas/env.clj` - Production settings
* `env/test/clj/saas/env.clj` - Test settings

### Understanding Signal Kinds

Telemere uses "signal kinds" to distinguish log sources:

* `:log` - Logs from Telemere's `log!` macro (your Clojure code)
* `:slf4j` - Logs from Java libraries via SLF4J

When configuring levels, you need to specify the correct kind:

```clojure
;; For your Clojure code (uses log!)
(t/set-min-level! :log "saas.*" :debug)

;; For Java libraries (use SLF4J)
(t/set-min-level! :slf4j "com.zaxxer.hikari.*" :warn)
```

### Silencing Noisy Dependencies

When you add a new Java dependency that's too chatty, silence it by adding to the appropriate `configure-*-log-levels!` function:

```clojure
;; In env/dev/clj/saas/env.clj
(defn configure-dev-log-levels!
  []
  ;; ... existing config ...

  ;; Silence your new noisy dependency
  (t/set-min-level! :slf4j "com.noisy.library.*" :warn))
```

### Development Configuration

Development is configured to be verbose for application code but quiet for third-party libraries:

```clojure
(defn configure-dev-log-levels!
  []
  ;; Silence infrastructure noise
  (t/set-min-level! :slf4j "org.postgresql.*" :warn)
  (t/set-min-level! :slf4j "com.zaxxer.hikari.*" :warn)
  (t/set-min-level! :slf4j "org.httpkit.*" :warn)
  (t/set-min-level! :slf4j "io.methvin.watcher.*" :warn)

  ;; Keep application logs verbose
  (t/set-min-level! :log "saas.*" :debug))
```

### Production Configuration

Production keeps application logs at INFO:

```clojure
(defn configure-prod-log-levels!
  []
  ;; Infrastructure at reasonable levels
  (t/set-min-level! :slf4j "com.zaxxer.hikari.*" :info)

  ;; Application at INFO
  (t/set-min-level! :log "saas.*" :info))
```

### Test Configuration

Tests are configured to be as quiet as possible:

```clojure
(defn configure-test-log-levels!
  []
  ;; Silence everything except errors
  (t/set-min-level! :slf4j "com.zaxxer.hikari.*" :error)
  (t/set-min-level! :slf4j "org.postgresql.*" :error)

  ;; App logs at INFO
  (t/set-min-level! :log "saas.*" :info))
```

***

## Sensitive Data Redaction

The logging middleware automatically redacts sensitive fields from request parameters:

```clojure
(def default-redact-key?
  #{:authorization :password :token :secret :secret-key
    :secret-token :refresh-token :confirm-password
    :access-token :id-token :csrf-token})
```

Logged as:

```json
{
  "params": {
    "user/email": "user@example.com",
    "user/password": "[REDACTED]"
  }
}
```

### Adding Custom Redaction

Edit `src/clj/saas/logging.clj`:

```clojure
(def default-redact-key?
  #{:authorization :password :token :secret
    :api-key :credit-card :ssn}) ; Add more keys
```

***

## JSON File Logging

In production, logs are written to JSON files for easy querying.

### Configuration

The JSON file handler is configured in `env/prod/clj/saas/env.clj`:

```clojure
(logging/add-json-file-handler!
  {:path "logs/saas-app.json"
   :interval :daily           ; :daily, :weekly, or :monthly
   :max-file-size (* 1024 1024 100) ; 100MB
   :max-num-parts 10          ; Parts per interval
   :max-num-intervals 30})    ; Days to keep
```

### Log Format

Each line is a single JSON object:

```json
{
  "timestamp": "2025-10-16T06:30:22.287897Z",
  "level": "info",
  "id": "api-call/response",
  "msg": "POST /api/login => 200",
  "ns": "saas.logging",
  "line": 71,
  "data": {
    "status": 200,
    "saas.logging/ms": 45
  },
  "ctx": {
    "req-id": "5efd277d-1ee7-46f8-94d7-93131c0d628a",
    "request-method": "post",
    "uri": "/api/login"
  }
}
```

***

## Common jq Queries

```bash
# Filter by request ID
cat logs/saas-app.json | jq -s --arg id "YOUR-REQ-ID" \
  '.[] | select(.ctx."req-id" == $id)'

# Find slow requests (>1000ms)
cat logs/saas-app.json | jq -s \
  '.[] | select(.data."saas.logging/ms" > 1000)'

# Count errors by type
cat logs/saas-app.json | jq -s \
  '[.[] | select(.level == "error")] | group_by(.id) | map({id: .[0].id, count: length})'
```

***

## Adding Logging to Your Code

### In Handlers

```clojure
(defn handle-purchase [req]
  (let [{:keys [user-id product-id]} (:params req)]
    (t/log! {:level :info
             :id :purchase/start
             :data {:user-id user-id :product-id product-id}}
            "Processing purchase")

    ;; ... business logic ...

    (t/log! {:level :info
             :id :purchase/complete}
            "Purchase completed")))
```

### Error Logging

```clojure
(try
  (process-payment! payment)
  (catch Exception e
    (t/log! {:level :error
             :id :payment/failed
             :error e
             :data {:payment-id (:id payment)}}
            "Payment processing failed")))
```

***

## Debugging Logging Issues

### Check Active Handlers

```clojure
(t/get-handlers)
;; => {:console #<...>, :json-file #<...>}
```

### Check Current Filters

```clojure
(t/get-filters)
;; Shows all active filters
```

### Test Signal Creation

```clojure
(t/with-signal
  (t/log! :info "Test message"))
;; => {:level :info, :msg "Test message", ...}
```

### Temporarily Disable Filters

```clojure
(t/without-filters
  (t/log! :debug "This will always appear"))
```

***

## Further Reading

* [Telemere Documentation](https://github.com/taoensso/telemere)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://shipclojure.gitbook.io/shipclojure-docs/backend/logging.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
