Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## CI - updates

- Fixed deprecated job versions in the CI
## [0.10.0] - 2025-08-12

- API and dashboard can now run on separate addresses (#46).

## [0.9.0] - 2024-05-30

Expand Down
7 changes: 7 additions & 0 deletions cos_alerter/alerter.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ def reload(self):
self.data["notify"]["repeat_interval"]
).total_seconds()

# if dashboard address key is missing, set it to None
dashboard_addr = None
try:
dashboard_addr = self.data["dashboard_listen_addr"]
except KeyError:
self.data["dashboard_listen_addr"] = dashboard_addr

# Static variables. We define them here so it is easy to expose them later as config
# values if needed.
base_dir = xdg_base_dirs.xdg_state_home() / "cos_alerter"
Expand Down
5 changes: 5 additions & 0 deletions cos_alerter/config-defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ log_level: "info"
# The address to listen on for http traffic.
# Format HOST:PORT
web_listen_addr: "0.0.0.0:8080"

# Optional: Separate address for the dashboard UI.
# If not set, dashboard will be served on the same address as the API.
# Format HOST:PORT
# dashboard_listen_addr: "127.0.0.1:8081"
66 changes: 53 additions & 13 deletions cos_alerter/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from .alerter import AlerterState, config, send_test_notification, up_time
from .logging import LEVELS, init_logging
from .server import app
from .server import create_app

logger = logging.getLogger("cos_alerter.daemon")

Expand Down Expand Up @@ -98,21 +98,61 @@ def main(run_for: Optional[int] = None, argv: List[str] = sys.argv):
if not str(e) == "signal only works in main thread of the main interpreter":
raise # pragma: no cover

# Start the web server.
# Start the web server(s).
# Starting in a thread rather than a new process allows waitress to inherit the log level
# from the daemon. It also facilitates communication over memory rather than files.
# clear_untrusted_proxy_headers is set to suppress a DeprecationWarning.
server_thread = threading.Thread(
target=waitress.serve,
args=(app,),
kwargs={
"clear_untrusted_proxy_headers": True,
"listen": config["web_listen_addr"],
},
)
server_thread.daemon = True # Makes this thread exit when the main thread exits.
logger.info("Starting the web server thread.")
server_thread.start()
# If dashboard_lister_addr exists, serve api and dashboard in their own respective addresses

dashboard_listen_addr = config["dashboard_listen_addr"]
web_listen_addr = config["web_listen_addr"]

if dashboard_listen_addr:
logger.info(
"Starting API server on %s, dashboard on %s",
config["web_listen_addr"],
dashboard_listen_addr,
)

# API server
api_app = create_app(include_api=True, include_dashboard=False)
api_server_thread = threading.Thread(
target=waitress.serve,
args=(api_app,),
kwargs={
"clear_untrusted_proxy_headers": True,
"listen": web_listen_addr,
},
)
api_server_thread.daemon = True
api_server_thread.start()

# Dashboard server
dashboard_app = create_app(include_api=False, include_dashboard=True)
dashboard_server_thread = threading.Thread(
target=waitress.serve,
args=(dashboard_app,),
kwargs={
"clear_untrusted_proxy_headers": True,
"listen": dashboard_listen_addr,
},
)
dashboard_server_thread.daemon = True
dashboard_server_thread.start()

else:
logger.info("Starting API server and dashboard on %s", config["web_listen_addr"])
app = create_app(include_api=True, include_dashboard=True)
server_thread = threading.Thread(
target=waitress.serve,
args=(app,),
kwargs={
"clear_untrusted_proxy_headers": True,
"listen": web_listen_addr,
},
)
server_thread.daemon = True
server_thread.start()

for clientid in config["watch"]["clients"]:
client_thread = threading.Thread(target=client_loop, args=(clientid,))
Expand Down
37 changes: 32 additions & 5 deletions cos_alerter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,41 @@

from .alerter import AlerterState, config, now_datetime

app = Flask(__name__)
metrics = PrometheusMetrics(app)
logger = logging.getLogger(__name__)


@app.route("/", methods=["GET"])
def create_app(include_api: bool = True, include_dashboard: bool = True) -> Flask:
"""Create Flask app with specified endpoints.

Args:
include_api: Whether to include the /alive API endpoint
include_dashboard: Whether to include the / dashboard endpoint

Returns:
Flask application instance
"""
app = Flask(__name__)
metrics = PrometheusMetrics(app) # noqa: F841

if include_dashboard:

@app.route("/", methods=["GET"])
def dashboard_route():
return dashboard()

if include_api:

@app.route("/alive", methods=["POST"])
def alive_route():
return alive()

@app.before_request
def log_request_wrapper():
return log_request()

return app


def dashboard():
"""Endpoint for the COS Alerter dashboard."""
clients = []
Expand All @@ -40,7 +69,6 @@ def dashboard():
return render_template("dashboard.html", clients=clients)


@app.route("/alive", methods=["POST"])
def alive():
"""Endpoint for Alertmanager instances to send their heartbeat alerts."""
# TODO Decide if we should validate the request.
Expand Down Expand Up @@ -74,7 +102,6 @@ def alive():
return "Success!"


@app.before_request
def log_request():
"""Log every HTTP request."""
logger.info(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "cos-alerter"
version = "0.9.0"
version = "0.10.0"
authors = [
{ name="Dylan Stephano-Shachter", email="dylan.stephano-shachter@canonical.com" }
]
Expand Down
2 changes: 1 addition & 1 deletion rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: cos-alerter
summary: A liveness checker for self-monitoring.
description: Receive regular pings from the cos stack and alert when they stop.
version: "0.9.0"
version: "0.10.0"
base: ubuntu@22.04
license: Apache-2.0
platforms:
Expand Down
21 changes: 20 additions & 1 deletion tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from werkzeug.datastructures import MultiDict

from cos_alerter.alerter import AlerterState, config
from cos_alerter.server import app
from cos_alerter.server import create_app

PARAMS = {"clientid": "clientid1", "key": "clientkey1"}


@pytest.fixture
def flask_client():
app = create_app()
return app.test_client()


Expand Down Expand Up @@ -111,3 +112,21 @@ def test_multiple_key_values(flask_client, fake_fs, state_init):
)
assert response.status_code == 400
assert len(response.data) > 0


def test_create_app_api_only(fake_fs, state_init):
app_instance = create_app(include_api=True, include_dashboard=False)
client = app_instance.test_client()

# Only API endpoint should be available
assert client.get("/").status_code == 404
assert client.post("/alive", query_string=PARAMS).status_code == 200


def test_create_app_dashboard_only(fake_fs, state_init):
app_instance = create_app(include_api=False, include_dashboard=True)
client = app_instance.test_client()

# Only dashboard endpoint should be available
assert client.get("/").status_code == 200
assert client.post("/alive", query_string=PARAMS).status_code == 404
Loading