Skip to main content

Overview

The Zitadel Ruby SDK provides built-in debugging capabilities to help you troubleshoot integration issues, inspect HTTP requests/responses, and understand SDK behavior. Debug mode logs detailed information about API calls, including request parameters, headers, and response bodies.

Enabling Debug Mode

During Initialization

Enable debugging when creating the client:
require 'zitadel-client'

client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  "your-access-token"
) do |config|
  config.debugging = true
end

With Any Authentication Method

# Private Key JWT
client = Zitadel::Client::Zitadel.with_private_key(
  "https://example.us1.zitadel.cloud",
  "path/to/key.json"
) do |config|
  config.debugging = true
end

# Client Credentials
client = Zitadel::Client::Zitadel.with_client_credentials(
  "https://example.us1.zitadel.cloud",
  "client-id",
  "client-secret"
) do |config|
  config.debugging = true
end
Debug mode is controlled by the debugging attribute in lib/zitadel/client/configuration.rb:51

Debug Output

When debugging is enabled, the SDK logs:
  1. HTTP request body (before sending)
  2. HTTP response body (after receiving)
  3. HTTP request/response details (via Typhoeus verbose mode)

Example Output

client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  "token"
) do |config|
  config.debugging = true
end

response = client.users.add_human_user(
  Zitadel::Client::UserServiceAddHumanUserRequest.new(
    username: "john.doe",
    profile: Zitadel::Client::UserServiceSetHumanProfile.new(
      given_name: "John",
      family_name: "Doe"
    ),
    email: Zitadel::Client::UserServiceSetHumanEmail.new(
      email: "[email protected]"
    )
  )
)
Console output:
HTTP request body param ~BEGIN~
{
  "username": "john.doe",
  "profile": {
    "givenName": "John",
    "familyName": "Doe"
  },
  "email": {
    "email": "[email protected]"
  }
}
~END~

HTTP response body ~BEGIN~
{
  "userId": "123456789",
  "details": {
    "sequence": "1",
    "creationDate": "2024-01-15T10:30:00Z"
  }
}
~END~
Request and response logging is implemented in lib/zitadel/client/api_client.rb:71 and api_client.rb:120

Custom Logger

Using a Custom Logger Instance

Provide your own logger for more control over output format and destination:
require 'logger'
require 'zitadel-client'

# Create custom logger
custom_logger = Logger.new('log/zitadel_debug.log')
custom_logger.level = Logger::DEBUG
custom_logger.formatter = proc do |severity, datetime, progname, msg|
  "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
end

client = Zitadel::Client::Zitadel.with_private_key(
  "https://example.us1.zitadel.cloud",
  "key.json"
) do |config|
  config.debugging = true
  config.logger = custom_logger
end

Rails Logger Integration

# config/initializers/zitadel.rb
require 'zitadel-client'

ZITADEL_CLIENT = Zitadel::Client::Zitadel.with_private_key(
  Rails.application.credentials.dig(:zitadel, :host),
  Rails.root.join('config', 'zitadel-key.json').to_s
) do |config|
  config.debugging = Rails.env.development?
  config.logger = Rails.logger
end
Rails log output:
[2024-01-15 10:30:45] DEBUG -- : HTTP request body param ~BEGIN~
{"limit":10}
~END~

[2024-01-15 10:30:46] DEBUG -- : HTTP response body ~BEGIN~
{"result":[...]}
~END~

Environment-Based Logging

require 'zitadel-client'

client = Zitadel::Client::Zitadel.with_access_token(
  ENV['ZITADEL_HOST'],
  ENV['ZITADEL_TOKEN']
) do |config|
  # Enable debugging based on environment variable
  config.debugging = ENV['ZITADEL_DEBUG'] == 'true'
  
  # Use different log levels per environment
  if ENV['RAILS_ENV'] == 'production'
    config.logger = Logger.new('log/zitadel.log')
    config.logger.level = Logger::WARN
  else
    config.logger = Logger.new(STDOUT)
    config.logger.level = Logger::DEBUG
  end
end

Troubleshooting Common Issues

Authentication Failures

Enable debug logging to inspect authentication headers and token exchange:
client = Zitadel::Client::Zitadel.with_client_credentials(
  "https://example.us1.zitadel.cloud",
  ENV['CLIENT_ID'],
  ENV['CLIENT_SECRET']
) do |config|
  config.debugging = true
end

begin
  response = client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 5)
  )
rescue Zitadel::Client::ApiError => e
  puts "Error Code: #{e.code}"
  puts "Response: #{e.response_body}"
  # Check debug logs for authentication headers
end
What to look for:
  • Missing or malformed Authorization header
  • Expired access tokens
  • Incorrect token type (Bearer vs. other)
  1. Enable debugging
    config.debugging = true
    config.logger = Logger.new(STDOUT)
    
  2. Make a simple API call
    begin
      client.users.list_users(
        Zitadel::Client::UserServiceListUsersRequest.new(limit: 1)
      )
    rescue => e
      puts "Error: #{e.message}"
    end
    
  3. Check debug output for:
    • HTTP status code (should see in response)
    • Authorization header format
    • Token presence and format
  4. Common issues:
    • 401 Unauthorized: Token is invalid or expired
    • 403 Forbidden: Token is valid but lacks permissions
    • Empty Authorization header: Authenticator not configured correctly

Network Timeout Issues

Increase timeout and enable debugging to understand slow requests:
client = Zitadel::Client::Zitadel.with_private_key(
  "https://example.us1.zitadel.cloud",
  "key.json"
) do |config|
  config.timeout = 60  # Increase to 60 seconds
  config.debugging = true
end

begin
  start_time = Time.now
  response = client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 1000)
  )
  puts "Request completed in #{Time.now - start_time} seconds"
rescue RuntimeError => e
  if e.message == 'Connection timed out'
    puts "Request exceeded 60 seconds"
    # Consider pagination or increasing timeout further
  end
end
Timeout errors are raised as RuntimeError with message "Connection timed out" from lib/zitadel/client/api_client.rb:75

SSL Certificate Issues

Debug SSL verification problems:
client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  "token"
) do |config|
  config.debugging = true
  config.verify_ssl = true
  config.verify_ssl_host = true
end

begin
  client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 1)
  )
rescue RuntimeError => e
  if e.message.include?('SSL')
    puts "SSL Error: #{e.message}"
    # Check certificate validity or provide custom CA cert
  end
end
Solutions:
  1. Provide custom CA certificate:
    config.ssl_ca_cert = "/path/to/ca-bundle.crt"
    
  2. For development only (not recommended for production):
    config.verify_ssl = false if Rails.env.development?
    

Request/Response Inspection

Inspect malformed requests or unexpected responses:
require 'json'

client = Zitadel::Client::Zitadel.with_private_key(
  "https://example.us1.zitadel.cloud",
  "key.json"
) do |config|
  config.debugging = true
end

request = Zitadel::Client::UserServiceAddHumanUserRequest.new(
  username: "test.user",
  profile: Zitadel::Client::UserServiceSetHumanProfile.new(
    given_name: "Test",
    family_name: "User"
  )
)

# Inspect request object before sending
puts "Request object: #{request.to_hash.to_json}"

begin
  response = client.users.add_human_user(request)
  puts "Response object: #{response.to_hash.to_json}"
rescue Zitadel::Client::ApiError => e
  puts "API returned error:"
  puts "  Status: #{e.code}"
  puts "  Body: #{e.response_body}"
  
  # Parse error details
  begin
    error_details = JSON.parse(e.response_body)
    puts "  Message: #{error_details['message']}"
    puts "  Details: #{error_details['details']}"
  rescue JSON::ParserError
    puts "  Raw error: #{e.response_body}"
  end
end

Security Considerations

Debug logs may contain sensitive information including:
  • Access tokens and authentication credentials
  • Personal user data (emails, names, etc.)
  • Internal system details
Never enable debug mode in production unless absolutely necessary, and always secure your logs.

Best Practices

  1. Use environment variables to control debugging:
    config.debugging = ENV['ZITADEL_DEBUG'] == 'true'
    
  2. Separate log files for sensitive data:
    if config.debugging
      config.logger = Logger.new('log/zitadel_debug.log')
      # Set appropriate file permissions
      File.chmod(0600, 'log/zitadel_debug.log')
    end
    
  3. Redact sensitive fields in production logs:
    class RedactingLogger < Logger
      def debug(message)
        redacted = message.gsub(/"Authorization"\s*=>\s*"[^"]+"/, '"Authorization" => "[REDACTED]"')
        super(redacted)
      end
    end
    
    config.logger = RedactingLogger.new('log/zitadel.log')
    
  4. Rotate and purge debug logs regularly:
    config.logger = Logger.new(
      'log/zitadel.log',
      10,           # Keep 10 log files
      10_485_760    # 10 MB per file
    )
    

Debugging Strategies

Isolate the Problem

# Create minimal reproducible example
require 'zitadel-client'

client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  ENV['ZITADEL_TOKEN']
) do |config|
  config.debugging = true
  config.timeout = 30
end

# Test simplest operation first
begin
  puts "Testing basic connectivity..."
  response = client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 1)
  )
  puts "✓ Connection successful"
  puts "✓ Authentication successful"
  puts "✓ API responding normally"
rescue => e
  puts "✗ Error: #{e.class} - #{e.message}"
end

Compare Working vs. Non-Working Requests

# Working request (baseline)
working_request = Zitadel::Client::UserServiceGetUserByIdRequest.new(
  user_id: "known-valid-id"
)

# Non-working request
failing_request = Zitadel::Client::UserServiceGetUserByIdRequest.new(
  user_id: "problematic-id"
)

# Compare debug output
[working_request, failing_request].each do |request|
  begin
    puts "\n=== Testing user_id: #{request.user_id} ==="
    response = client.users.get_user_by_id(request)
    puts "Success: #{response.user_id}"
  rescue => e
    puts "Failed: #{e.message}"
  end
end

Capture Full Request Details

require 'zitadel-client'
require 'json'

class DebugWrapper
  def initialize(client)
    @client = client
  end
  
  def method_missing(method, *args, &block)
    service = @client.send(method)
    
    # Wrap service methods
    service.define_singleton_method(:method_missing) do |service_method, *service_args|
      puts "\n=== API Call Debug ==="
      puts "Service: #{method}"
      puts "Method: #{service_method}"
      puts "Arguments: #{service_args.map(&:to_hash).to_json}"
      puts "Timestamp: #{Time.now.iso8601}"
      
      begin
        result = super(service_method, *service_args)
        puts "Result: Success"
        result
      rescue => e
        puts "Result: Error - #{e.class}"
        puts "Error Details: #{e.message}"
        raise
      end
    end
    
    service
  end
end

# Usage
base_client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  ENV['ZITADEL_TOKEN']
) { |c| c.debugging = true }

client = DebugWrapper.new(base_client)

client.users.list_users(
  Zitadel::Client::UserServiceListUsersRequest.new(limit: 5)
)

Getting Help

If you’re still experiencing issues after enabling debug mode:
  1. Collect debug logs showing the full request/response
  2. Check the Zitadel Community Discord for support
  3. Report issues on GitHub with:
    • Ruby version
    • SDK version
    • Debug logs (redact sensitive data)
    • Steps to reproduce
See the README section on debugging: /workspace/source/README.md:194-206

Next Steps

Build docs developers (and LLMs) love