From 68f3acded898f7a5912cca92d253c4212d33fed8 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Thu, 28 May 2026 18:49:41 -0400 Subject: [PATCH] Fix dashboard rendering: barchart panel type + metadata off the table prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cdk/stacks/pipeline_stack.py | 6 +++++- grafana/dashboards/apm-work-orders.json | 4 ++-- lambdas/classifier/handler.py | 9 ++++++--- lambdas/slack_post/handler.py | 6 +++--- lambdas/slack_post/interactions.py | 2 +- lambdas/slack_post/slackio.py | 7 ++++--- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cdk/stacks/pipeline_stack.py b/cdk/stacks/pipeline_stack.py index aa38888..88765e1 100644 --- a/cdk/stacks/pipeline_stack.py +++ b/cdk/stacks/pipeline_stack.py @@ -274,6 +274,9 @@ class PipelineStack(Stack): # Parquet to S3 — it never touches the catalog (Phase 3). self.exports_bucket.grant_read(self.classifier_fn, "raw/*") self.exports_bucket.grant_read_write(self.classifier_fn, "analytics/*") + # summary.json/details.json live under meta/ (kept out of the Athena + # table's analytics/ prefix so queries don't read JSON as Parquet). + self.exports_bucket.grant_read_write(self.classifier_fn, "meta/*") secretsmanager.Secret.from_secret_name_v2( self, "AnthropicKey", ANTHROPIC_SECRET ).grant_read(self.classifier_fn) @@ -335,7 +338,8 @@ class PipelineStack(Stack): ), ), ) - self.exports_bucket.grant_read(fn, "analytics/*") + # Slack Lambdas read only the daily JSON under meta/ (not the Parquet). + self.exports_bucket.grant_read(fn, "meta/*") slack_secret.grant_read(fn) dashboard_param.grant_read(fn) return fn diff --git a/grafana/dashboards/apm-work-orders.json b/grafana/dashboards/apm-work-orders.json index ffecd14..4a9f09f 100644 --- a/grafana/dashboards/apm-work-orders.json +++ b/grafana/dashboards/apm-work-orders.json @@ -179,7 +179,7 @@ "panels": [ { "id": 1, - "type": "bar-chart", + "type": "barchart", "title": "Category Distribution", "description": "Count of work orders by classification category for the selected snapshot date. Sorted descending by volume. Filter using the template variables above.", "gridPos": { "x": 0, "y": 0, "w": 14, "h": 9 }, @@ -366,7 +366,7 @@ }, { "id": 4, - "type": "bar-chart", + "type": "barchart", "title": "Escalations by Site", "description": "Total escalations per site for the selected snapshot date. Includes all escalation categories.", "gridPos": { "x": 8, "y": 9, "w": 16, "h": 8 }, diff --git a/lambdas/classifier/handler.py b/lambdas/classifier/handler.py index f356cac..f678c33 100644 --- a/lambdas/classifier/handler.py +++ b/lambdas/classifier/handler.py @@ -30,7 +30,8 @@ import pandas as pd import classify as clf -ANALYTICS_PREFIX = "analytics" +ANALYTICS_PREFIX = "analytics" # Parquet snapshots — the Glue/Athena table reads this +META_PREFIX = "meta" # summary.json/details.json — kept OUT of the table's prefix # Map snapshot field -> substring matched (case-insensitively) against the export # header, tolerating minor header drift in the 13-column APM export. @@ -237,17 +238,19 @@ def handler(event, context): mode="overwrite_partitions", ) + # summary.json/details.json go under meta/ — NOT analytics/. Athena reads + # every object in the table's prefix as Parquet, so JSON there breaks queries. summary = _build_summary(df, dt, key, blank) _s3.put_object( Bucket=bucket, - Key=f"{ANALYTICS_PREFIX}/dt={dt}/summary.json", + Key=f"{META_PREFIX}/dt={dt}/summary.json", Body=json.dumps(summary, indent=2).encode("utf-8"), ContentType="application/json", ) # details.json — per-WO index the slack-post Lambda reads for drill-down modals. _s3.put_object( Bucket=bucket, - Key=f"{ANALYTICS_PREFIX}/dt={dt}/details.json", + Key=f"{META_PREFIX}/dt={dt}/details.json", Body=json.dumps(_build_details(df)).encode("utf-8"), ContentType="application/json", ) diff --git a/lambdas/slack_post/handler.py b/lambdas/slack_post/handler.py index 128b851..b0c54bb 100644 --- a/lambdas/slack_post/handler.py +++ b/lambdas/slack_post/handler.py @@ -25,11 +25,11 @@ def handler(event, context): """Post the daily summary and (conditionally) the 3rd-escalation alert.""" dt = (event or {}).get("dt") or datetime.now(timezone.utc).strftime("%Y-%m-%d") - today = slackio.read_analytics_json(dt, "summary.json") + today = slackio.read_meta_json(dt, "summary.json") if today is None: print(f"No summary.json for dt={dt}; nothing to post.") return {"posted": False, "reason": "no summary", "dt": dt} - yesterday = slackio.read_analytics_json(_yesterday(dt), "summary.json") + yesterday = slackio.read_meta_json(_yesterday(dt), "summary.json") client = slackio.web_client() channel = slackio.channel_id() @@ -44,7 +44,7 @@ def handler(event, context): # Standalone batched alert — only when there are 3rd escalations. Pull the # rows from details.json so the alert can name the WOs. if today.get("third_escalation_count", 0) > 0: - details = slackio.read_analytics_json(dt, "details.json") or [] + details = slackio.read_meta_json(dt, "details.json") or [] thirds = [d for d in details if d.get("category") == "3rd Escalation"] alert_blocks = blockkit.build_escalation_alert(thirds) if alert_blocks: diff --git a/lambdas/slack_post/interactions.py b/lambdas/slack_post/interactions.py index b664fda..5df79f3 100644 --- a/lambdas/slack_post/interactions.py +++ b/lambdas/slack_post/interactions.py @@ -60,7 +60,7 @@ def handler(event, context): # The daily post is same-day; default the drill to today's snapshot. dt = datetime.now(timezone.utc).strftime("%Y-%m-%d") - details = slackio.read_analytics_json(dt, "details.json") or [] + details = slackio.read_meta_json(dt, "details.json") or [] wos = [d for d in details if d.get(field) == value] view = blockkit.build_wo_modal( diff --git a/lambdas/slack_post/slackio.py b/lambdas/slack_post/slackio.py index 6b4b670..6d0a102 100644 --- a/lambdas/slack_post/slackio.py +++ b/lambdas/slack_post/slackio.py @@ -60,9 +60,10 @@ def verify_signature(body: str, timestamp: str, signature: str) -> bool: return verifier.is_valid(body=body, timestamp=timestamp, signature=signature) -def read_analytics_json(dt: str, name: str): - """Read ``analytics/dt=