1import type * as cloudwatch from "@distilled.cloud/aws/cloudwatch";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Binding from "../../Binding.ts";
5import { makeBatchedSink } from "../internal/BatchedSink.ts";
6import { isBindingHost } from "../Lambda/Function.ts";
7import {
8 MetricSink,
9 type MetricSinkDatum,
10 type MetricSinkProps,
11} from "./MetricSink.ts";
12import { PutMetricData } from "./PutMetricData.ts";
13
14const encoder = new TextEncoder();
15
16/**
17 * PutMetricData accepts up to 1 MB per POST; approximate each datum's wire
18 * size by its JSON encoding and leave headroom for the request envelope
19 * (`Action`, `Namespace`, form-encoding overhead).
20 */
21const MAX_BYTES = 1_000_000;
22
23export const MetricSinkHttp = Layer.effect(
24 MetricSink,
25 Effect.gen(function* () {
26 const putMetricData = yield* PutMetricData;
27
28 return Effect.fn(function* (props: MetricSinkProps) {
29 if (!globalThis.__ALCHEMY_RUNTIME__) {
30 const host = yield* Binding.Host;
31 if (isBindingHost(host)) {
32 yield* host.bind`Allow(${host}, AWS.CloudWatch.MetricSink(${props.Namespace}))`(
33 {
34 policyStatements: [
35 {
36 Effect: "Allow",
37 // PutMetricData does not support resource-level permissions.
38 Action: ["cloudwatch:PutMetricData"],
39 Resource: ["*"],
40 },
41 ],
42 },
43 );
44 }
45 }
46 const put = yield* putMetricData();
47 return makeBatchedSink<
48 MetricSinkDatum,
49 cloudwatch.PutMetricDataResponse,
50 cloudwatch.PutMetricDataError
51 >({
52 maxRecords: 1000,
53 maxBytes: MAX_BYTES,
54 sizeOf: (datum) => encoder.encode(JSON.stringify(datum)).length,
55 send: (batch) =>
56 put({
57 Namespace: props.Namespace,
58 MetricData: [...batch],
59 }),
60 // PutMetricData is all-or-nothing — no per-datum partial failures to
61 // extract, so there is no `unprocessed`/`rejected` handling here.
62 });
63 });
64 }),
65);