Module: Mailkube::Logging

Defined in:
lib/mailkube/logging.rb,
sig/mailkube/logging.rbs

Overview

Opt-in request logging: silent by default, and never a secret on the way out.

Deliberately not built on stdlib Logger. logger stopped being a default gem in Ruby 4.0, so require "logger" inside this gem raises under Bundler on a Ruby this gem supports, and declaring it would cost the gem its zero-dependency claim — the same trap that keeps Base64 out of Serialization. A library also has no business installing handlers, levels or formatters on its host's behalf.

So the sink is any object responding to #write(String): $stderr, a File, a StringIO, or a two-line shim over your application's own logger. That is the same dependency-inversion seam Client.new(http:) already uses, and it is what makes Mailkube.enable_logging(device: Rails.logger) work without this gem knowing what Rails is.

This is the one piece of mutable module state in the gem. Logging.enable! and Logging.disable! are boot-time calls from the main thread; assigning one object reference is atomic, and no request path ever writes here.

Constant Summary collapse

ENV_LEVEL =

Environment variable that turns logging on without a code change. It holds a level.

Returns:

  • (String)
"MAILKUBE_LOG"
VERBOSE_LEVELS =

The MAILKUBE_LOG values verbose enough to let this SDK's records through.

MAILKUBE_LOG is a level, not a flag, in every mailkube SDK. This one emits exactly one class of record, the request/response trace, and that record is debug-level — so a level more selective than debug must silence it. MAILKUBE_LOG=warning is a working way to say "logs, but not from the SDK", and it has to keep working here or the variable means something different in Ruby than in python, node, Go and PHP.

Returns:

  • (Array[String])
%w[trace debug all].freeze
SENSITIVE_HEADERS =

The headers whose values are masked before anything is written.

Returns:

  • (Array[String])
%w[authorization idempotency-key].freeze
REDACTION =

What a masked header value is replaced with.

Returns:

  • (String)
"***"

Class Method Summary collapse

Class Method Details

.device#write?

Returns where SDK logging is written, or nil when it is off.

Returns:

  • (#write, nil)

    where SDK logging is written, or nil when it is off.



39
# File 'lib/mailkube/logging.rb', line 39

def self.device = @device

.disable!nil

Turn logging back off. The inverse of enable!, for a suite that turned it on.

Returns:

  • (nil)

    always.



50
# File 'lib/mailkube/logging.rb', line 50

def self.disable! = @device = nil

.enable!(device: $stderr) ⇒ #write

Turn logging on.

Parameters:

  • device (#write) (defaults to: $stderr)

    where to write; anything responding to #write(String).

  • device: (_LogDevice) (defaults to: $stderr)

Returns:

  • (#write)

    the device now in use.



45
# File 'lib/mailkube/logging.rb', line 45

def self.enable!(device: $stderr) = @device = device

.enable_from_env(env = ENV) ⇒ #write?

Turn logging on from the environment, when MAILKUBE_LOG names a level of VERBOSE_LEVELS.

Called once from mailkube.rb, so a deployment can turn logging on without a code change. It takes the environment as an argument rather than reading ENV inline because a bare if at the bottom of a file runs exactly once at load: no spec could reach its second branch, and the branch-coverage gate would then be paying for a line nobody can test.

An unrecognized value leaves logging off rather than raising. This runs at require time, and no environment variable should be able to make require "mailkube" fail.

Parameters:

  • env (Hash) (defaults to: ENV)

    the environment to read.

  • (Hash[String, String])

Returns:

  • (#write, nil)

    the device now in use, or nil when the level does not admit this SDK.



64
65
66
67
68
69
# File 'lib/mailkube/logging.rb', line 64

def self.enable_from_env(env = ENV)
  level = env[ENV_LEVEL]
  return nil unless VERBOSE_LEVELS.include?(level.to_s.strip.downcase)

  enable!
end

.redact(headers) ⇒ Hash{String => String}

A copy of headers with every secret value masked, safe to write anywhere.

Accumulated into an annotated hash rather than returned from to_h { [k, v] }, for the same reason as Serialization.query: Steep types a two-element array literal in block-body position as Array[String], not as the tuple to_h declares.

Parameters:

  • headers (Hash{String => String})

    the headers about to be logged.

  • (Hash[String, String])

Returns:

  • (Hash{String => String})

    the redacted copy; the caller's hash is not modified.



79
80
81
82
83
# File 'lib/mailkube/logging.rb', line 79

def self.redact(headers)
  masked = {} #: Hash[String, String]
  headers.each { |name, value| masked[name] = SENSITIVE_HEADERS.include?(name.downcase) ? REDACTION : value }
  masked
end

.request(method, url, headers) ⇒ void

This method returns an undefined value.

Log one outgoing request, with its headers redacted.

The &. is not a nil-check bolted onto a log line: it is "silent by default", and it is why redact costs nothing for the callers who never enable logging — which is what lets this call sit on the request path at all.

Parameters:

  • method (String)

    the HTTP method.

  • url (String)

    the absolute URL.

  • headers (Hash{String => String})

    the merged request headers.

  • (String)
  • (String)
  • (Hash[String, String])


95
96
97
# File 'lib/mailkube/logging.rb', line 95

def self.request(method, url, headers)
  @device&.write("mailkube > #{method} #{url} #{redact(headers)}\n")
end

.response(status, url, request_id = nil) ⇒ void

This method returns an undefined value.

Log one response, including the server's request id when it sent one.

The id is what makes a support ticket traceable: the customer finds the failing call in their own logs and quotes the same value the API recorded. It is an id, not content — no recipient, subject or body reaches a log record here or anywhere else in this module.

Parameters:

  • status (Integer)

    the HTTP status code.

  • url (String)

    the absolute URL.

  • request_id (String, nil) (defaults to: nil)

    the X-Request-Id header, when the response carried one.

  • (Integer)
  • (String)
  • (String, nil)


109
110
111
# File 'lib/mailkube/logging.rb', line 109

def self.response(status, url, request_id = nil)
  @device&.write("mailkube < #{status} #{url}#{request_id.nil? ? "" : " request_id=#{request_id}"}\n")
end