commit 0986c40b5077f1a08d01a7c44be600ae566a3172
parent 555df61ee112a712a11099699c348ac063360be9
Author: Asher Morgan <asher@ashermorgan.net>
Date: Tue, 8 Sep 2026 20:02:25 -0700
Add logging in aoh/server.py
Also improve server error handling and add $AOH_LOG_LEVEL option.
Diffstat:
4 files changed, 39 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
@@ -53,14 +53,16 @@ environment variables or a `.env` file.
- `AOH_DEBUG`: If set to `1`, the AoH server will copy Ansible logs to stdout.
Defaults to `0`.
+- `AOH_LOG_LEVEL`: The AoH logging threshold. Defaults to `ERROR`.
+
+- `AOH_MAX_RUNNERS`: The maximum number of concurrent runners, or `0` for no
+ limit. Defaults to `0`.
+
- `AOH_ORIGIN`: The origin of the AoH server, as referenced in the `/run.py`
script (e.g. `https://aoh.example.com:5000`). This must be set if running AoH
behind a proxy. Defaults to the origin on which requests for `/run.py` are
received.
-- `AOH_MAX_RUNNERS`: The maximum number of concurrent runners, or `0` for no
- limit. Defaults to `0`.
-
- `AOH_PASSWORDS_FILE`: The file containing passwords for password-protected
playbooks (see below). Defaults to `/aoh/passwords.yml` when running the
Docker image and `./passwords.yml` otherwise.
diff --git a/aoh/__init__.py b/aoh/__init__.py
@@ -1,5 +1,11 @@
+import logging
+
+from .config import AOH_LOG_LEVEL
from .server import app, scheduler
+logging.basicConfig()
+logging.getLogger(__name__).setLevel(AOH_LOG_LEVEL)
+
def create_app():
scheduler.start()
diff --git a/aoh/config.py b/aoh/config.py
@@ -10,6 +10,8 @@ load_dotenv()
AOH_DEBUG = (os.getenv('AOH_DEBUG', '0') == '1')
+AOH_LOG_LEVEL = os.getenv('AOH_LOG_LEVEL', 'ERROR')
+
try:
AOH_MAX_RUNNERS = int(os.getenv('AOH_MAX_RUNNERS', '0'))
except ValueError:
diff --git a/aoh/server.py b/aoh/server.py
@@ -1,11 +1,12 @@
+import logging
import os
from flask import Flask, abort, render_template, request, session
from flask_apscheduler import APScheduler
from .config import AOH_MAX_RUNNERS, AOH_ORIGIN
-from .playbook import get_playbook
-from .runner import Runner
+from .playbook import PlaybookError, get_playbook
+from .runner import Runner, RunnerError
from .security import *
_RUNNERS = {}
@@ -20,6 +21,8 @@ app.secret_key = os.urandom(16)
scheduler = APScheduler()
scheduler.init_app(app)
+logger = logging.getLogger(__name__)
+
@scheduler.task('interval', seconds=_GC_INTERVAL)
def gc():
@@ -28,6 +31,8 @@ def gc():
runners = list(_RUNNERS.items())
for id, runner in runners:
if runner and runner.has_timed_out(_GC_THRESHOLD):
+ logger.info(f'{runner.playbook.name} (#{id[:8]}): Runner torn down '
+ 'due to inactivity.')
runner.teardown()
_RUNNERS.pop(id, None)
@@ -39,6 +44,13 @@ def inject_stage_and_region():
}
+@app.errorhandler(RunnerError)
+@app.errorhandler(PlaybookError)
+def error(e):
+ logger.error(e)
+ return {'msg': 'Internal server error'}, 500
+
+
@app.get('/run')
@app.get('/run.py')
def install():
@@ -51,10 +63,8 @@ def new_runner(path):
if not playbook:
return {'err': f'Playbook not found: {path}'}, 400
- if not validate_args(playbook, request.json['args']):
- return {'err': 'Bad or banned arguments passed.'}, 400
-
if AOH_MAX_RUNNERS != 0 and len(_RUNNERS) >= AOH_MAX_RUNNERS:
+ logger.info(f'{path}: No runners available.')
return { 'err': 'No runners available.' }, 503
exp_pw_types = get_required_passwords(playbook, request.json['args'])
@@ -68,16 +78,23 @@ def new_runner(path):
}, 401
if not validate_aoh_password(playbook, act_pws.get('aoh_password')):
+ logger.info(f'{path}: Received incorrect password.')
return {'err': 'Incorrect AoH password.'}, 401
act_pws.pop('aoh_password', None) # Don't pass AoH password on to runner
+ if not validate_args(playbook, request.json['args']):
+ logger.info(f"{path}: Received bad arguments: {request.json['args']}")
+ return {'err': 'Bad or banned arguments passed.'}, 400
+
runner = Runner(playbook, request.json['host'], request.json['args'],
act_pws)
assert runner.id is not None
_RUNNERS[runner.id] = runner
session['runner'] = runner.id
- short_id = runner.id.split('-')[0]
+ short_id = runner.id[:8]
+
+ logger.info(f'{path} (#{short_id}): Created runner.')
return {}, 201, {'Location': f'/runners/{path}/{short_id}'}
@@ -95,9 +112,12 @@ def runner_update(path, short_id):
if runner.playbook.name != path:
return {'err': f'Playbook not found: {path}'}, 400
+ logger.debug(f'{path} (#{short_id}): Received client message.')
res = runner.process_message(request.json)
if res['finished']:
+ logger.info(f'{runner.playbook.name} (#{short_id}): Runner torn down '
+ 'due to completion.')
runner.teardown()
_RUNNERS.pop(id, None)