gale.log

gale.log sets up logging for a gale game: printed to the terminal and written to a plain-text file by default, extensible to send records anywhere else (Graylog and friends) by attaching another handler. It’s a thin configuration layer over Python’s own logging module — every handler you attach already is the “where do records go” strategy the standard library gives you for free, so gale.log never reinvents formatting, levels, or record dispatch, only wires up sane defaults and returns real logging.Logger instances.

Basic usage

from gale.log import get_logger

logger = get_logger("space_trip")
logger.info("saucer destroyed a rock")
logger.warning("player near the edge of the screen")

Calling get_logger without ever calling configure first still works — it configures itself with the defaults (console + a gale.log file in the current directory) the first time it’s called. Call configure yourself, once, near the start of your game, to override those defaults:

from gale.log import configure
from gale.log.level import LogLevel

configure(level=LogLevel.DEBUG, log_file="my_game.log", console=True)

Pass log_file=None to disable file logging, or console=False to disable printing to the terminal.

Adding another destination (Graylog and friends)

Any logging.Handler can be attached on top of the console/file defaults with add_handler — that’s the whole extension point:

from gale.log import add_handler
from gale.log.graylog_handler import GraylogHandler

add_handler(GraylogHandler("graylog.example.com", 12201))

GraylogHandler sends each record as a GELF (Graylog Extended Log Format) datagram over UDP. Anything passed as extra to a logging call becomes a custom GELF field:

logger.info("player scored", extra={"player_id": 7, "points": 100})

Two more destinations ship the same way: SentryHandler sends each record to Sentry (sentry.io or a self-hosted instance) as an event, using only its DSN and the standard library, no sentry-sdk dependency:

from gale.log.sentry_handler import SentryHandler

add_handler(SentryHandler("https://<public_key>@<host>/<project_id>"))

and DiscordWebhookHandler posts each record to a Discord channel through an incoming webhook — a common, zero-infrastructure way for a small/solo team to get pinged about errors. Since pinging a channel for every INFO line would be noisy, it’s typically attached with a raised level:

from gale.log.level import LogLevel
from gale.log.discord_webhook_handler import DiscordWebhookHandler

handler = DiscordWebhookHandler(
    "https://discord.com/api/webhooks/<id>/<token>", username="space_trip"
)
handler.setLevel(LogLevel.WARNING)
add_handler(handler)

For anything else — a different log aggregator, a custom webhook, whatever — write a small logging.Handler subclass the same way and hand it to add_handler; gale.log doesn’t need to know about it specifically.

Named loggers

logger = get_logger("nightwatch.guard")  # -> "gale.nightwatch.guard"

Nesting names under gale’s own top-level logger keeps every gale game’s log lines identifiable and lets you filter/configure a whole subsystem’s verbosity independently if you ever need to (through the standard library’s own logger-hierarchy features — get_logger returns a plain logging.Logger, so everything else in the logging documentation applies unchanged).