> For the complete documentation index, see [llms.txt](https://azutopia.gitbook.io/azu/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://azutopia.gitbook.io/azu/middleware/add-logging.md).

# Add Logging

This guide shows you how to implement logging in your Azu application.

## Built-in Logger

Use Azu's built-in logger handler:

```crystal
MyApp.start [
  Azu::Handler::Logger.new,
  # ... other handlers
]
```

## Configure Log Level

Set the log level based on environment:

```crystal
Azu.configure do |config|
  case ENV.fetch("AZU_ENV", "development")
  when "production"
    config.log.level = Log::Severity::Info
  when "test"
    config.log.level = Log::Severity::Warn
  else
    config.log.level = Log::Severity::Debug
  end
end
```

## Custom Logger Handler

Create a structured logger:

```crystal
class StructuredLogger < Azu::Handler::Base
  def call(context)
    start = Time.instant
    request_id = context.request.headers["X-Request-ID"]?

    begin
      call_next(context)
    ensure
      duration = Time.instant - start
      log_request(context, duration, request_id)
    end
  end

  private def log_request(context, duration, request_id)
    Log.info { {
      request_id: request_id,
      method: context.request.method,
      path: context.request.path,
      status: context.response.status_code,
      duration_ms: duration.total_milliseconds.round(2),
      remote_ip: client_ip(context),
      user_agent: context.request.headers["User-Agent"]?
    }.to_json }
  end

  private def client_ip(context) : String
    context.request.headers["X-Forwarded-For"]?.try(&.split(",").first.strip) ||
      context.request.remote_address.to_s
  end
end
```

## Logging in Endpoints

Log within your endpoints:

```crystal
struct CreateUserEndpoint
  include Azu::Endpoint(CreateUserRequest, UserResponse)

  post "/users"

  def call : UserResponse
    Log.debug { "Creating user with email: #{create_user_request.email}" }

    user = User.create!(create_user_request)

    Log.info { "User created: id=#{user.id}, email=#{user.email}" }

    UserResponse.new(user)
  rescue ex
    Log.error(exception: ex) { "Failed to create user" }
    raise ex
  end
end
```

## Log Backends

### File Backend

```crystal
Log.setup do |config|
  file_backend = Log::IOBackend.new(File.new("log/app.log", "a"))

  config.bind "azu.*", :info, file_backend
  config.bind "*", :info, file_backend
end
```

### JSON Backend

```crystal
class JsonLogBackend < Log::Backend
  def initialize(@io : IO = STDOUT)
  end

  def write(entry : Log::Entry)
    data = {
      timestamp: entry.timestamp.to_rfc3339,
      severity: entry.severity.to_s,
      source: entry.source,
      message: entry.message,
      data: entry.data.to_h,
    }

    if ex = entry.exception
      data = data.merge({
        exception: ex.class.name,
        exception_message: ex.message,
        backtrace: ex.backtrace?.try(&.first(10))
      })
    end

    @io.puts data.to_json
  end
end

Log.setup do |config|
  config.bind "*", :info, JsonLogBackend.new
end
```

### Multiple Backends

```crystal
Log.setup do |config|
  stdout = Log::IOBackend.new
  file = Log::IOBackend.new(File.new("log/app.log", "a"))

  # Development: stdout only
  if ENV["AZU_ENV"]? == "development"
    config.bind "*", :debug, stdout
  else
    # Production: file for all, stdout for errors
    config.bind "*", :info, file
    config.bind "*", :error, stdout
  end
end
```

## Request Context Logging

Include request context in all logs:

```crystal
class ContextLogger
  Log = ::Log.for(self)

  def self.with_context(request_id : String, user_id : Int64? = nil, &)
    Log.context.set(request_id: request_id)
    Log.context.set(user_id: user_id.to_s) if user_id

    yield
  ensure
    Log.context.clear
  end
end

# Usage in handler
class RequestContextHandler < Azu::Handler::Base
  def call(context)
    request_id = context.request.headers["X-Request-ID"]? || UUID.random.to_s
    user_id = context.request.headers["X-User-ID"]?.try(&.to_i64)

    ContextLogger.with_context(request_id, user_id) do
      call_next(context)
    end
  end
end
```

## Error Logging

Log errors with full context:

```crystal
class ErrorLogger < Azu::Handler::Base
  Log = ::Log.for(self)

  def call(context)
    call_next(context)
  rescue ex
    log_error(context, ex)
    raise ex
  end

  private def log_error(context, ex : Exception)
    Log.error(exception: ex) { {
      error: ex.class.name,
      message: ex.message,
      path: context.request.path,
      method: context.request.method,
      request_id: context.request.headers["X-Request-ID"]?
    }.to_json }
  end
end
```

## Sensitive Data Filtering

Filter sensitive data from logs:

```crystal
module LogFilter
  SENSITIVE_KEYS = ["password", "token", "secret", "api_key", "authorization"]

  def self.filter(data : Hash) : Hash
    data.transform_values do |value|
      case value
      when Hash
        filter(value)
      when String
        SENSITIVE_KEYS.any? { |k| data.keys.any?(&.downcase.includes?(k)) } ? "[FILTERED]" : value
      else
        value
      end
    end
  end
end
```

## Log Rotation

Use logrotate for production:

```
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 appuser appuser
}
```

## Performance Logging

Log slow requests:

```crystal
class SlowRequestLogger < Azu::Handler::Base
  THRESHOLD = 1.second

  def call(context)
    start = Time.instant
    call_next(context)
    duration = Time.instant - start

    if duration > THRESHOLD
      Log.warn { "Slow request: #{context.request.method} #{context.request.path} took #{duration.total_seconds.round(2)}s" }
    end
  end
end
```

## See Also

* [Create Custom Middleware](/azu/middleware/create-custom-middleware.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://azutopia.gitbook.io/azu/middleware/add-logging.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
