Repository navigation
Add zero-cost observability and consolidated Slack alerts - #33
Conversation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
There was a problem hiding this comment.
Code Review
This pull request refactors the application architecture by removing public API Gateway endpoints in favor of IAM-authorized direct Lambda invocations and EventBridge schedules, adding CloudFormation-managed log groups with a 30-day retention policy, and introducing a robust observability and exception sanitization framework. Additionally, scrapers and clients were updated to track slot-level processing outcomes with structured logging. The review feedback highlights two key improvement opportunities: optimizing the dormitory scraping handler by avoiding asyncio.run() inside a loop to prevent resource exhaustion, and using get_text(strip=True) in the HTML table parser to ensure clean dictionary keys.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| for parsed_menu in parsed_menus: | ||
| asyncio.run( | ||
| notification_service.send_date_summary( | ||
| _summary_from_parsed(parsed_menu) | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Calling asyncio.run() inside a loop creates and destroys a new event loop for every single iteration. This is highly inefficient and can lead to resource exhaustion or connection issues if the HTTP client/connection pool is tied to the loop lifecycle. Consider wrapping the loop in an async helper function and running it once using asyncio.run(), utilizing asyncio.gather for concurrent execution.
async def send_all_summaries():
await asyncio.gather(*(
notification_service.send_date_summary(
_summary_from_parsed(parsed_menu)
)
for parsed_menu in parsed_menus
))
asyncio.run(send_all_summaries())There was a problem hiding this comment.
Fixed in commit 8491ea4. Introduced a single async helper _dormitory_scrape_and_notify that handles both the scrape and all notification sends in one coroutine, so the handler calls asyncio.run() exactly once regardless of how many menu slots are returned. Sends remain sequential (one await notification_service.send_date_summary() per parsed menu) rather than concurrent — this avoids Slack burst/rate-limit issues that asyncio.gather could trigger when multiple summaries arrive simultaneously. Two new tests assert the single-event-loop contract: test_dormitory_scraping_handler_uses_single_asyncio_run (success path, 2 menus → 1 asyncio.run call) and test_dormitory_scraping_handler_domain_error_uses_single_asyncio_run (domain error path → still 1 asyncio.run call). Both pass.
| continue | ||
| td_tag = tr_tag.find('td', attrs={'class': 'menu_nm'}) | ||
| if isinstance(td_tag, Tag): | ||
| menu_nm_dict[td_tag.get_text()] = tr_tag |
There was a problem hiding this comment.
td_tag.get_text() returns the raw text of the table cell, which may contain leading/trailing whitespaces or newlines depending on the HTML formatting. Using get_text(strip=True) ensures clean keys for the dictionary.
| menu_nm_dict[td_tag.get_text()] = tr_tag | |
| menu_nm_dict[td_tag.get_text(strip=True)] = tr_tag |
There was a problem hiding this comment.
Fixed in commit 6d67b88. Applied get_text(strip=True) as suggested — the one-character change eliminates leading/trailing whitespace and newlines from dictionary keys. Added a regression test (test_parse_table_to_dict_strips_whitespace_from_keys) that covers both multi-line indented text and HTML entity newlines ( ), asserting that keys like '중식1' and '석식1' are present without surrounding whitespace. All 26 tests in test_utils.py + test_lambda_handlers.py pass.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add zero-cost observability and consolidated Slack alerts
Summary
Introduces a structured, zero-cost observability layer across the food-crawling pipeline. All instrumentation is implemented via pure Python dataclasses and enums — no external APM, no additional AWS services, no added cost. Slack notifications are consolidated to one summary message per date instead of per-restaurant, reducing noise and API calls.
Key Changes
Structured Observability Contracts
ObservationResult,ScrapingOutcome,SchedulingOutcome) for all pipeline stagesSUCCESS,PARTIAL,EMPTY,SKIPPED,FAILEDSafe Exception and Retry Handling
Consolidated Slack Notifications
Step Functions Correlation
Infrastructure Hardening
Validation
Deployment Note
Not yet deployed. Before applying Terraform:
sam buildon Apple Silicon — this is a local build artifact and does not affect deployed runtime behavior.Commits (15)
4304b2cAdd structured observability contracts687a87eSanitize restaurant exception payloadsba323f2Reset observability dependency context9ed7b71Harden parsing utility fallbacksedd36c7Record time slot strategy outcomes972a5adClassify scraper source outcomesc0643b4Summarize Slack notifications by date5ba1bb3Instrument existing client retries6411ac9Aggregate scraping outcomesc102115Aggregate scheduling outcomes23a37a7Normalize scheduling handler events731e14cNormalize scraping handler eventsad54111Sanitize terminal failure notifications77abecdSecure workflow infrastructure contracts774d050Expand deterministic observability CI coverage