mirror of
https://github.com/Sea-Haven-Industries/apm-wo-analysis.git
synced 2026-09-30 11:13:14 +00:00
Two issues found loading the deployed dashboard: 1. Panels used type "bar-chart" (hyphenated); Grafana's core panel is "barchart" — hence "plugin bar-chart required". Fixed both panels. 2. ALL panels showed "no data" because Athena failed with HIVE_BAD_DATA: the classifier wrote summary.json/details.json INTO analytics/dt=*/ — the same prefix the Glue table scans — so Athena tried to read the JSON as Parquet and every query failed. Move the metadata to a separate meta/dt=*/ prefix: classifier writes there (grant_read_write meta/*), the Slack Lambdas read there (read_meta_json, grant_read meta/*), and analytics/ holds only Parquet. Verified: the category GROUP BY query now succeeds against Athena.
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
"""Shared Slack + AWS I/O for the post and interactions Lambdas.
|
|
|
|
Keeps the Block Kit builders (blockkit.py) pure: everything that touches the
|
|
network or AWS lives here. Slack credentials are a single Secrets Manager secret
|
|
``{ botToken, signingSecret, channelId }``; the Grafana dashboard URL is
|
|
operational config in SSM (editable without a redeploy). Both are cached for the
|
|
life of the execution environment.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
|
|
import boto3
|
|
from slack_sdk import WebClient
|
|
from slack_sdk.signature import SignatureVerifier
|
|
|
|
_secrets = boto3.client("secretsmanager")
|
|
_ssm = boto3.client("ssm")
|
|
_s3 = boto3.client("s3")
|
|
|
|
_SECRET_NAME = os.environ["SLACK_SECRET_NAME"]
|
|
_DASHBOARD_PARAM = os.environ["DASHBOARD_URL_PARAM"]
|
|
_BUCKET = os.environ["ANALYTICS_BUCKET"]
|
|
|
|
# Lazily-populated caches (warm across invocations in the same container).
|
|
_creds: dict | None = None
|
|
_dashboard_url: str | None = None
|
|
|
|
|
|
def get_credentials() -> dict:
|
|
"""Return the Slack creds dict: ``botToken``, ``signingSecret``, ``channelId``."""
|
|
global _creds
|
|
if _creds is None:
|
|
raw = _secrets.get_secret_value(SecretId=_SECRET_NAME)["SecretString"]
|
|
_creds = json.loads(raw)
|
|
return _creds
|
|
|
|
|
|
def get_dashboard_url() -> str:
|
|
"""Grafana dashboard URL for the 📊 button / modal overflow links (SSM)."""
|
|
global _dashboard_url
|
|
if _dashboard_url is None:
|
|
_dashboard_url = _ssm.get_parameter(Name=_DASHBOARD_PARAM)["Parameter"]["Value"]
|
|
return _dashboard_url
|
|
|
|
|
|
def web_client() -> WebClient:
|
|
return WebClient(token=get_credentials()["botToken"])
|
|
|
|
|
|
def channel_id() -> str:
|
|
return get_credentials()["channelId"]
|
|
|
|
|
|
def verify_signature(body: str, timestamp: str, signature: str) -> bool:
|
|
"""Validate a Slack request signature (HMAC + 5-minute replay window)."""
|
|
verifier = SignatureVerifier(signing_secret=get_credentials()["signingSecret"])
|
|
return verifier.is_valid(body=body, timestamp=timestamp, signature=signature)
|
|
|
|
|
|
def read_meta_json(dt: str, name: str):
|
|
"""Read ``meta/dt=<dt>/<name>`` as JSON, or None if absent. (summary.json /
|
|
details.json live under meta/, kept out of the Athena table's analytics/ prefix.)"""
|
|
key = f"meta/dt={dt}/{name}"
|
|
try:
|
|
obj = _s3.get_object(Bucket=_BUCKET, Key=key)
|
|
except _s3.exceptions.NoSuchKey:
|
|
return None
|
|
return json.loads(obj["Body"].read())
|