Add alertmanager as supported sender and update deps
This commit is contained in:
parent
05ffc640ed
commit
973e1fd789
18 changed files with 1682 additions and 1155 deletions
57
ops_bot/alertmanager.py
Normal file
57
ops_bot/alertmanager.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import logging
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from ops_bot.common import COLOR_ALARM, COLOR_OK, COLOR_UNKNOWN
|
||||
from ops_bot.config import RoutingKey
|
||||
|
||||
|
||||
def prometheus_alert_to_markdown(
|
||||
alert_data: Dict, # type: ignore[type-arg]
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Converts a prometheus alert json to markdown
|
||||
"""
|
||||
messages = []
|
||||
known_labels = ["alertname", "instance", "job"]
|
||||
for alert in alert_data["alerts"]:
|
||||
title = (
|
||||
alert["annotations"]["description"]
|
||||
if hasattr(alert["annotations"], "description")
|
||||
else alert["annotations"]["summary"]
|
||||
)
|
||||
severity = alert.get("severity", "critical")
|
||||
if alert["status"] == "resolved":
|
||||
color = COLOR_OK
|
||||
elif severity == "critical":
|
||||
color = COLOR_ALARM
|
||||
else:
|
||||
color = COLOR_UNKNOWN
|
||||
|
||||
status = alert["status"]
|
||||
generatorURL = alert.get("generatorURL")
|
||||
plain = f"{status}: {title}"
|
||||
header_str = f"{status.upper()}[{status}]"
|
||||
formatted = f"<strong><font color={color}>{header_str}</font></strong>: [{title}]({generatorURL})"
|
||||
for label_name in known_labels:
|
||||
try:
|
||||
plain += "\n* **{0}**: {1}".format(
|
||||
label_name.capitalize(), alert["labels"][label_name]
|
||||
)
|
||||
formatted += "\n* **{0}**: {1}".format(
|
||||
label_name.capitalize(), alert["labels"][label_name]
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error("error parsing labels", exc_info=e)
|
||||
pass
|
||||
messages.append((plain, formatted))
|
||||
return messages
|
||||
|
||||
|
||||
async def parse_alertmanager_event(
|
||||
route: RoutingKey,
|
||||
payload: Any,
|
||||
request: Request,
|
||||
) -> List[Tuple[str, str]]:
|
||||
return prometheus_alert_to_markdown(payload)
|
||||
|
|
@ -4,7 +4,7 @@ from typing import Any
|
|||
|
||||
import click
|
||||
|
||||
from ops_bot.config import RoutingKey, load_config, save_config
|
||||
from ops_bot.config import RoutingKey, hook_types, load_config, save_config
|
||||
|
||||
|
||||
@click.group()
|
||||
|
|
@ -28,20 +28,23 @@ def cli(ctx: Any, config_file: str) -> None:
|
|||
)
|
||||
@click.option("--room-id", help="The room ID to send the messages to")
|
||||
@click.pass_obj
|
||||
def add_hook(config_file: str, name: str, hook_type: str, room_id: str) -> None:
|
||||
def add_hook(config_file: str, name: str, maybe_hook_type: str, room_id: str) -> None:
|
||||
settings = load_config(config_file)
|
||||
path_key = secrets.token_urlsafe(30)
|
||||
secret_token = secrets.token_urlsafe(30)
|
||||
if name in set([key.name for key in settings.routing_keys]):
|
||||
print("Error: A hook with that name already exists")
|
||||
sys.exit(1)
|
||||
if maybe_hook_type not in hook_types:
|
||||
print(f"Invalid hook type {maybe_hook_type}")
|
||||
print("Must be one of ", ", ".join(hook_types))
|
||||
settings.routing_keys.append(
|
||||
RoutingKey(
|
||||
name=name,
|
||||
path_key=path_key,
|
||||
secret_token=secret_token,
|
||||
room_id=room_id,
|
||||
hook_type=hook_type,
|
||||
hook_type=maybe_hook_type, # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
save_config(settings)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,37 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from typing import List, Literal
|
||||
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from ops_bot.matrix import MatrixClientSettings
|
||||
|
||||
env_path = os.getenv("BOT_ENV_FILE")
|
||||
|
||||
|
||||
HookType = Literal["gitlab", "pagerduty", "aws-sns", "alertmanager"]
|
||||
hook_types = typing.get_args(HookType)
|
||||
|
||||
|
||||
class RoutingKey(BaseSettings):
|
||||
name: str
|
||||
path_key: str
|
||||
secret_token: str
|
||||
room_id: str
|
||||
hook_type: Literal["gitlab", "pagerduty", "aws-sns"]
|
||||
hook_type: HookType
|
||||
|
||||
|
||||
class BotSettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BOT_", env_file=env_path, env_file_encoding="utf-8"
|
||||
)
|
||||
routing_keys: List[RoutingKey]
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
||||
matrix: MatrixClientSettings
|
||||
|
||||
class Config:
|
||||
env_prefix = "BOT_"
|
||||
case_sensitive = False
|
||||
|
||||
def get_rooms(self) -> List[str]:
|
||||
return list(set([key.room_id for key in self.routing_keys]))
|
||||
|
||||
|
|
@ -35,9 +42,10 @@ def config_file_exists(filename: str) -> bool:
|
|||
|
||||
def load_config(filename: str = "config.json") -> BotSettings:
|
||||
if config_file_exists(filename):
|
||||
bot_settings = BotSettings.parse_file(filename)
|
||||
with open(filename, "r") as f:
|
||||
bot_settings = BotSettings.model_validate_json(f.read())
|
||||
else:
|
||||
bot_settings = BotSettings(_env_file=".env", _env_file_encoding="utf-8")
|
||||
bot_settings = BotSettings() # type: ignore[call-arg]
|
||||
logging.getLogger().setLevel(bot_settings.log_level)
|
||||
return bot_settings
|
||||
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ async def handle_event(x_gitlab_event: str, payload: Any) -> List[Tuple[str, str
|
|||
**attr.asdict(subevt, recurse=False),
|
||||
**{key: getattr(subevt, key) for key in subevt.event_properties},
|
||||
"abort": abort,
|
||||
**base_args, # type: ignore
|
||||
**base_args,
|
||||
}
|
||||
args["templates"] = templates.proxy(args) # type: ignore
|
||||
args["templates"] = templates.proxy(args)
|
||||
|
||||
html = tpl.render(**args)
|
||||
if not html or aborted:
|
||||
|
|
@ -87,4 +87,6 @@ async def parse_event(
|
|||
route: RoutingKey, payload: Any, request: Request
|
||||
) -> List[Tuple[str, str]]:
|
||||
x_gitlab_event = request.headers.get("x-gitlab-event")
|
||||
return await handle_event(x_gitlab_event, payload)
|
||||
if x_gitlab_event:
|
||||
return await handle_event(x_gitlab_event, payload)
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Protocol, Tuple, cast
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncIterator, Dict, List, Optional, Protocol, Tuple, cast
|
||||
|
||||
import json_logging
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
||||
from fastapi.security import (
|
||||
HTTPAuthorizationCredentials,
|
||||
|
|
@ -13,18 +15,11 @@ from fastapi.security import (
|
|||
HTTPBearer,
|
||||
)
|
||||
|
||||
from ops_bot import aws, pagerduty
|
||||
from ops_bot import alertmanager, aws, pagerduty
|
||||
from ops_bot.config import BotSettings, RoutingKey, load_config
|
||||
from ops_bot.gitlab import hook as gitlab_hook
|
||||
from ops_bot.matrix import MatrixClient
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
bearer_security = HTTPBearer(auto_error=False)
|
||||
basic_security = HTTPBasic(auto_error=False)
|
||||
|
||||
|
||||
async def get_matrix_service(request: Request) -> MatrixClient:
|
||||
"""A helper to fetch the matrix client from the app state"""
|
||||
|
|
@ -37,21 +32,29 @@ async def matrix_main(matrix_client: MatrixClient) -> None:
|
|||
await asyncio.gather(*workers)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event() -> None:
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
|
||||
config_fname = os.environ.get("BOT_CONFIG_FILE", "config.yaml")
|
||||
bot_settings = load_config(config_fname)
|
||||
c = MatrixClient(settings=bot_settings.matrix, join_rooms=bot_settings.get_rooms())
|
||||
app.state.matrix_client = c
|
||||
app.state.bot_settings = bot_settings
|
||||
asyncio.create_task(matrix_main(c))
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event() -> None:
|
||||
yield
|
||||
await app.state.matrix_client.shutdown()
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
bearer_security = HTTPBearer(auto_error=False)
|
||||
basic_security = HTTPBasic(auto_error=False)
|
||||
|
||||
log = logging.getLogger("ops_bot")
|
||||
log.addHandler(logging.StreamHandler(sys.stdout))
|
||||
json_logging.init_fastapi(enable_json=True)
|
||||
json_logging.init_request_instrument(app)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, str]:
|
||||
return {"message": "Hello World"}
|
||||
|
|
@ -113,6 +116,7 @@ handlers: Dict[str, Tuple[Authorizer, ParseHandler]] = {
|
|||
"gitlab": (gitlab_hook.authorize, gitlab_hook.parse_event),
|
||||
"pagerduty": (bearer_token_authorizer, pagerduty.parse_pagerduty_event),
|
||||
"aws-sns": (nop_authorizer, aws.parse_sns_event),
|
||||
"alertmanager": (nop_authorizer, alertmanager.parse_alertmanager_event),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import List, Optional, Protocol
|
||||
|
||||
from markdown import markdown
|
||||
from nio import AsyncClient, AsyncClientConfig, LoginResponse
|
||||
from pydantic import BaseModel, BaseSettings
|
||||
from pydantic import BaseModel
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ClientCredentials(BaseModel):
|
||||
|
|
@ -72,6 +74,9 @@ class MatrixClient:
|
|||
|
||||
self.greeting_sent = False
|
||||
|
||||
if self.store_path and not os.path.isdir(self.store_path):
|
||||
os.mkdir(self.store_path)
|
||||
|
||||
async def start(self) -> None:
|
||||
await self.login()
|
||||
|
||||
|
|
@ -95,9 +100,9 @@ class MatrixClient:
|
|||
|
||||
async def login_fresh(self) -> None:
|
||||
self.client = AsyncClient(
|
||||
self.settings.homeserver,
|
||||
self.settings.user_id,
|
||||
self.settings.store_path,
|
||||
homeserver=self.settings.homeserver,
|
||||
user=self.settings.user_id,
|
||||
store_path=str(self.settings.store_path),
|
||||
config=self.client_config,
|
||||
ssl=self.settings.verify_ssl,
|
||||
)
|
||||
|
|
@ -117,11 +122,12 @@ class MatrixClient:
|
|||
|
||||
async def login_with_credentials(self) -> None:
|
||||
credentials = self.credential_store.read()
|
||||
|
||||
self.client = AsyncClient(
|
||||
credentials.homeserver,
|
||||
credentials.user_id,
|
||||
homeserver=credentials.homeserver,
|
||||
user=credentials.user_id,
|
||||
device_id=credentials.device_id,
|
||||
store_path=self.store_path,
|
||||
store_path=str(self.store_path),
|
||||
config=self.client_config,
|
||||
ssl=True,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue