mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 06:03:14 +00:00
83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
|
|
"""Shared CloudWatch EMF emitter for the procurement-ingest pipelines.
|
||
|
|
|
||
|
|
A single generic ``emit_metric`` builds the Embedded Metric Format envelope
|
||
|
|
(``_aws`` block + promoted dimension properties + the metric-value key) and
|
||
|
|
prints it to stdout, where the Lambda log subscription materializes the metric.
|
||
|
|
Both the PO and WO email processors emit their parse-outcome metric through
|
||
|
|
``emit_parse_outcome`` so the load-bearing dimension-set list
|
||
|
|
``[["ParseMethod"], ["ParseMethod", "TemplateId"]]`` is pinned in exactly ONE
|
||
|
|
place -- a one-sided dimension change to one pipeline is structurally
|
||
|
|
impossible.
|
||
|
|
|
||
|
|
Envelope byte-equivalence rests on CPython dict insertion order (preserved by
|
||
|
|
json.dumps with default separators): ``{**properties, metric_name: value}``
|
||
|
|
yields ``_aws`` first, then the caller's properties in order, then the metric
|
||
|
|
value last -- matching every inline emitter this module replaced.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
|
||
|
|
PARSE_METRIC_NAME = "ParseOutcome"
|
||
|
|
|
||
|
|
# Two dimension sets are published for every parse-outcome metric: ["ParseMethod"]
|
||
|
|
# (aggregated across all template ids -- the series the fallback-rate alarms
|
||
|
|
# query) AND ["ParseMethod", "TemplateId"] (per-template breakdown for Logs
|
||
|
|
# Insights / dashboards). CloudWatch materializes only the exact dimension sets
|
||
|
|
# listed here and does NOT auto-aggregate, so an alarm's single-dimension query
|
||
|
|
# would receive no data unless ["ParseMethod"] is emitted explicitly. Pinned
|
||
|
|
# ONCE here; both pipelines share it.
|
||
|
|
_PARSE_DIMENSION_SETS = [["ParseMethod"], ["ParseMethod", "TemplateId"]]
|
||
|
|
|
||
|
|
|
||
|
|
def emit_metric(
|
||
|
|
namespace, metric_name, dimension_sets, properties, *, value=1, unit="Count"
|
||
|
|
):
|
||
|
|
"""Print one CloudWatch EMF log line for ``metric_name`` in ``namespace``.
|
||
|
|
|
||
|
|
Zero-latency (no PutMetricData API call): the extraction path is async and
|
||
|
|
the role already has logs:PutLogEvents. ``properties`` are emitted in caller
|
||
|
|
order between the ``_aws`` block and the trailing metric-value key; the keys
|
||
|
|
named in ``dimension_sets`` are the promoted (dimensioned) fields, the rest
|
||
|
|
ride along as Logs-Insights-queryable properties.
|
||
|
|
"""
|
||
|
|
emf = {
|
||
|
|
"_aws": {
|
||
|
|
# EMF requires Timestamp (epoch ms); without it CloudWatch may not
|
||
|
|
# extract the metric datapoint from the log event.
|
||
|
|
"Timestamp": int(datetime.now(timezone.utc).timestamp() * 1000),
|
||
|
|
"CloudWatchMetrics": [
|
||
|
|
{
|
||
|
|
"Namespace": namespace,
|
||
|
|
"Dimensions": dimension_sets,
|
||
|
|
"Metrics": [{"Name": metric_name, "Unit": unit}],
|
||
|
|
}
|
||
|
|
],
|
||
|
|
},
|
||
|
|
**properties,
|
||
|
|
metric_name: value,
|
||
|
|
}
|
||
|
|
print(json.dumps(emf))
|
||
|
|
|
||
|
|
|
||
|
|
def emit_parse_outcome(namespace, method, template_id, reason_code, id_key, id_value):
|
||
|
|
"""Emit one parse-outcome EMF line shared by both email processors.
|
||
|
|
|
||
|
|
ParseMethod/TemplateId are the only promoted (dimensioned) fields to keep
|
||
|
|
cardinality low; ReasonCode and the pipeline id (``id_key``: ``po_number``
|
||
|
|
or ``work_order_id``) ride along as Logs-Insights-queryable properties. See
|
||
|
|
``_PARSE_DIMENSION_SETS`` for why both the single- and two-dimension sets
|
||
|
|
are published.
|
||
|
|
"""
|
||
|
|
emit_metric(
|
||
|
|
namespace,
|
||
|
|
PARSE_METRIC_NAME,
|
||
|
|
_PARSE_DIMENSION_SETS,
|
||
|
|
{
|
||
|
|
"ParseMethod": method,
|
||
|
|
"TemplateId": template_id or "unknown",
|
||
|
|
"ReasonCode": reason_code or "ok",
|
||
|
|
id_key: id_value or "",
|
||
|
|
},
|
||
|
|
)
|