You've already forked docker-ara
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f041df873 | |||
| 41d0f13ba8 |
@@ -15,8 +15,11 @@ ARA_DATABASE_PASSWORD=arasecret
|
||||
# ------------------------------------------------------------
|
||||
# ARA core
|
||||
# ------------------------------------------------------------
|
||||
# IMPORTANT: replace with a long random string in production
|
||||
ARA_SECRET_KEY=changeme_use_a_long_random_string
|
||||
# REQUIRED: generate with:
|
||||
# python3 -c "import secrets; print(secrets.token_hex(50))"
|
||||
# Without this, a new random key is generated on every container
|
||||
# start — invalidating Django sessions and ARA's signed cookies.
|
||||
ARA_SECRET_KEY=
|
||||
|
||||
# Timezone — controls BOTH crond scheduling AND ARA display times.
|
||||
# Use a tz database name: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
|
||||
@@ -37,7 +37,7 @@ Full reference: <https://ara.readthedocs.io/en/latest/api-configuration.html>
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `ARA_BASE_DIR` | `/opt/ara` | Data & config directory |
|
||||
| `ARA_SECRET_KEY` | *(random)* | Django secret key — **set a stable value in production** |
|
||||
| `ARA_SECRET_KEY` | *(random — **must be set**)* | Django secret key. If unset, a new key is generated every restart, invalidating sessions. Generate with: `python3 -c "import secrets; print(secrets.token_hex(50))"` |
|
||||
| `ARA_ALLOWED_HOSTS` | `["127.0.0.1","localhost","::1"]` | Hosts the server will respond to |
|
||||
| `TZ` | `UTC` | System timezone — controls **when crond fires** |
|
||||
| `ARA_TIME_ZONE` | same as `TZ` | Timezone for ARA to store/display results — keep in sync with `TZ` |
|
||||
|
||||
@@ -28,14 +28,15 @@ services:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${ARA_PORT:-8000}:${ARA_PORT:-8000}"
|
||||
volumes:
|
||||
- ara_data:/opt/ara
|
||||
environment:
|
||||
# -----------------------------------------------------------------------
|
||||
# Core
|
||||
# -----------------------------------------------------------------------
|
||||
ARA_BASE_DIR: /opt/ara
|
||||
ARA_SECRET_KEY: ${ARA_SECRET_KEY:-changeme_use_a_long_random_string}
|
||||
# SECRET_KEY must be set to a stable random value — if left unset a new
|
||||
# key is generated on every container start, invalidating Django sessions.
|
||||
# Generate one with: python3 -c "import secrets; print(secrets.token_hex(50))"
|
||||
ARA_SECRET_KEY: ${ARA_SECRET_KEY:?ARA_SECRET_KEY must be set in .env}
|
||||
ARA_ALLOWED_HOSTS: ${ARA_ALLOWED_HOSTS:-["*"]}
|
||||
ARA_TIME_ZONE: ${TZ:-UTC}
|
||||
ARA_LOG_LEVEL: ${ARA_LOG_LEVEL:-INFO}
|
||||
@@ -74,4 +75,3 @@ services:
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
ara_data:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM almalinux:9.5-minimal
|
||||
|
||||
ARG DEV_DEPENDENCIES="gcc python3-devel postgresql-devel mariadb-devel"
|
||||
ARG DEV_DEPENDENCIES="gcc python3-devel postgresql-devel mariadb-connector-c-devel"
|
||||
|
||||
# Install only the runtime packages we need, including cronie for cron support
|
||||
# tini is used as PID 1 to reap zombie processes spawned by crond and forward
|
||||
@@ -24,6 +24,7 @@ RUN microdnf install -y ${DEV_DEPENDENCIES} \
|
||||
&& microdnf clean all
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY docker/json_logger.py /json_logger.py
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -33,6 +34,7 @@ RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Core
|
||||
ENV ARA_BASE_DIR=/opt/ara
|
||||
ENV PYTHONPATH=/
|
||||
# ENV ARA_SECRET_KEY=changeme # set a stable secret in production
|
||||
# ENV ARA_ALLOWED_HOSTS="['*']" # restrict to your hostname(s)
|
||||
# ENV ARA_TIME_ZONE=UTC # ARA display/storage timezone
|
||||
|
||||
@@ -47,6 +47,6 @@ crond -n &
|
||||
exec python3 -m gunicorn \
|
||||
--workers="${ARA_GUNICORN_WORKERS:-4}" \
|
||||
--access-logfile - \
|
||||
--logger-class json_logger.JsonLogger \
|
||||
--bind "[::]:${ARA_PORT:-8000}" \
|
||||
--access-logformat '%({x-forwarded-for}i)s %l %u %t "%r" %s %b "%f" "%a"' \
|
||||
ara.server.wsgi
|
||||
|
||||
87
docker/json_logger.py
Normal file
87
docker/json_logger.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Minimal JSON logger for gunicorn.
|
||||
|
||||
Replaces gunicorn's default text logger with structured JSON output,
|
||||
one JSON object per line. Both access and error logs are covered.
|
||||
|
||||
Usage:
|
||||
gunicorn --logger-class json_logger.JsonLogger ...
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from gunicorn.glogging import Logger
|
||||
|
||||
|
||||
class JsonLogger(Logger):
|
||||
"""Gunicorn logger that emits one JSON object per log line."""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Error / application log records #
|
||||
# ------------------------------------------------------------------ #
|
||||
def setup(self, cfg):
|
||||
super().setup(cfg)
|
||||
# Replace every handler's formatter on both error and access loggers
|
||||
for logger_name in ("error_log", "access_log"):
|
||||
lgr = getattr(self, logger_name)
|
||||
for handler in lgr.handlers:
|
||||
handler.setFormatter(_JsonFormatter())
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Access log records #
|
||||
# ------------------------------------------------------------------ #
|
||||
def access(self, resp, req, environ, request_time):
|
||||
"""Emit a structured JSON access log record."""
|
||||
if not self.access_log.handlers or not self.cfg.accesslog:
|
||||
return
|
||||
|
||||
status = resp.status
|
||||
if isinstance(status, str):
|
||||
status_code = int(status.split(None, 1)[0])
|
||||
else:
|
||||
status_code = status
|
||||
|
||||
record = {
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
"level": "INFO",
|
||||
"logger": "gunicorn.access",
|
||||
"method": environ.get("REQUEST_METHOD", "-"),
|
||||
"path": environ.get("PATH_INFO", "-"),
|
||||
"query": environ.get("QUERY_STRING", "") or None,
|
||||
"status": status_code,
|
||||
"response_bytes": getattr(resp, "sent", None),
|
||||
"duration_ms": round(request_time.seconds * 1000
|
||||
+ request_time.microseconds / 1000, 2),
|
||||
"remote_addr": environ.get("REMOTE_ADDR", "-"),
|
||||
"x_forwarded_for": environ.get("HTTP_X_FORWARDED_FOR") or None,
|
||||
"user_agent": environ.get("HTTP_USER_AGENT", "-"),
|
||||
"referer": environ.get("HTTP_REFERER") or None,
|
||||
"http_version": environ.get("SERVER_PROTOCOL", "-"),
|
||||
}
|
||||
# Drop None values for cleaner output
|
||||
record = {k: v for k, v in record.items() if v is not None}
|
||||
|
||||
# Write directly to handler stream to avoid double-formatting
|
||||
line = json.dumps(record)
|
||||
for handler in self.access_log.handlers:
|
||||
stream = getattr(handler, "stream", None)
|
||||
if stream:
|
||||
stream.write(line + "\n")
|
||||
stream.flush()
|
||||
|
||||
|
||||
class _JsonFormatter(logging.Formatter):
|
||||
"""Formatter that converts a LogRecord to a single JSON line."""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
obj = {
|
||||
"timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
if record.exc_info:
|
||||
obj["exception"] = self.formatException(record.exc_info)
|
||||
return json.dumps(obj)
|
||||
Reference in New Issue
Block a user