apm-wo-analysis/lambdas/slack_post/slackio.py
Adam Moussa 68f3acded8 Fix dashboard rendering: barchart panel type + metadata off the table prefix
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.
2026-05-28 18:49:41 -04:00

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())