| 1 | import * as DevOpsGuru from "@/AWS/DevOpsGuru"; |
| 2 | import * as Lambda from "@/AWS/Lambda"; |
| 3 | import * as Duration from "effect/Duration"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Stream from "effect/Stream"; |
| 7 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 8 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 9 | import path from "pathe"; |
| 10 | |
| 11 | const main = path.resolve(import.meta.dirname, "handler.ts"); |
| 12 | |
| 13 | export class DevOpsGuruTestFunction extends Lambda.Function<Lambda.Function>()( |
| 14 | "DevOpsGuruTestFunction", |
| 15 | ) {} |
| 16 | |
| 17 | export default DevOpsGuruTestFunction.make( |
| 18 | { |
| 19 | main, |
| 20 | functionUrl: true, |
| 21 | timeout: Duration.seconds(30), |
| 22 | }, |
| 23 | Effect.gen(function* () { |
| 24 | // Event source: subscribe the host to insight lifecycle events. The |
| 25 | // deploy proves the EventBridge rule + invoke permission wiring. |
| 26 | yield* DevOpsGuru.consumeInsightEvents( |
| 27 | { kinds: ["new-insight", "severity-upgraded"] }, |
| 28 | (events) => |
| 29 | Stream.runForEach(events, (event) => |
| 30 | Effect.log( |
| 31 | `devops-guru insight: ${event.detail.insightId} (${event.detail.insightSeverity})`, |
| 32 | ), |
| 33 | ), |
| 34 | ); |
| 35 | |
| 36 | // Every account-level capability the service exposes — the init phase |
| 37 | // registers the IAM grant for each; a representative subset is |
| 38 | // exercised over HTTP routes below. |
| 39 | const bound = { |
| 40 | describeAccountHealth: yield* DevOpsGuru.DescribeAccountHealth(), |
| 41 | describeAccountOverview: yield* DevOpsGuru.DescribeAccountOverview(), |
| 42 | describeAnomaly: yield* DevOpsGuru.DescribeAnomaly(), |
| 43 | describeFeedback: yield* DevOpsGuru.DescribeFeedback(), |
| 44 | describeInsight: yield* DevOpsGuru.DescribeInsight(), |
| 45 | describeOrganizationHealth: |
| 46 | yield* DevOpsGuru.DescribeOrganizationHealth(), |
| 47 | describeOrganizationOverview: |
| 48 | yield* DevOpsGuru.DescribeOrganizationOverview(), |
| 49 | describeOrganizationResourceCollectionHealth: |
| 50 | yield* DevOpsGuru.DescribeOrganizationResourceCollectionHealth(), |
| 51 | describeResourceCollectionHealth: |
| 52 | yield* DevOpsGuru.DescribeResourceCollectionHealth(), |
| 53 | getCostEstimation: yield* DevOpsGuru.GetCostEstimation(), |
| 54 | startCostEstimation: yield* DevOpsGuru.StartCostEstimation(), |
| 55 | listAnomaliesForInsight: yield* DevOpsGuru.ListAnomaliesForInsight(), |
| 56 | listAnomalousLogGroups: yield* DevOpsGuru.ListAnomalousLogGroups(), |
| 57 | listEvents: yield* DevOpsGuru.ListEvents(), |
| 58 | listInsights: yield* DevOpsGuru.ListInsights(), |
| 59 | listMonitoredResources: yield* DevOpsGuru.ListMonitoredResources(), |
| 60 | listOrganizationInsights: yield* DevOpsGuru.ListOrganizationInsights(), |
| 61 | listRecommendations: yield* DevOpsGuru.ListRecommendations(), |
| 62 | putFeedback: yield* DevOpsGuru.PutFeedback(), |
| 63 | searchInsights: yield* DevOpsGuru.SearchInsights(), |
| 64 | searchOrganizationInsights: |
| 65 | yield* DevOpsGuru.SearchOrganizationInsights(), |
| 66 | deleteInsight: yield* DevOpsGuru.DeleteInsight(), |
| 67 | }; |
| 68 | |
| 69 | return { |
| 70 | fetch: Effect.gen(function* () { |
| 71 | const request = yield* HttpServerRequest; |
| 72 | const url = new URL(request.originalUrl); |
| 73 | const pathname = url.pathname; |
| 74 | |
| 75 | if (request.method === "GET" && pathname === "/bindings") { |
| 76 | return yield* HttpServerResponse.json({ |
| 77 | bound: Object.keys(bound), |
| 78 | }); |
| 79 | } |
| 80 | |
| 81 | // Account health counters — works even before DevOps Guru is |
| 82 | // configured (returns zeroes). |
| 83 | if (request.method === "GET" && pathname === "/health") { |
| 84 | const health = yield* bound.describeAccountHealth(); |
| 85 | return yield* HttpServerResponse.json({ |
| 86 | openReactiveInsights: health.OpenReactiveInsights, |
| 87 | openProactiveInsights: health.OpenProactiveInsights, |
| 88 | metricsAnalyzed: health.MetricsAnalyzed, |
| 89 | }); |
| 90 | } |
| 91 | |
| 92 | // Account overview for the trailing 24 hours. |
| 93 | if (request.method === "GET" && pathname === "/overview") { |
| 94 | const overview = yield* bound.describeAccountOverview({ |
| 95 | FromTime: new Date(Date.now() - 24 * 3600_000), |
| 96 | }); |
| 97 | return yield* HttpServerResponse.json({ |
| 98 | reactiveInsights: overview.ReactiveInsights, |
| 99 | proactiveInsights: overview.ProactiveInsights, |
| 100 | }); |
| 101 | } |
| 102 | |
| 103 | // Ongoing reactive insights. |
| 104 | if (request.method === "GET" && pathname === "/insights") { |
| 105 | const { ReactiveInsights } = yield* bound.listInsights({ |
| 106 | StatusFilter: { Ongoing: { Type: "REACTIVE" } }, |
| 107 | }); |
| 108 | return yield* HttpServerResponse.json({ |
| 109 | count: (ReactiveInsights ?? []).length, |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | // Search reactive insights in the trailing 24 hours. The service |
| 114 | // requires ToTime even though the SDK marks it optional. |
| 115 | if (request.method === "GET" && pathname === "/search") { |
| 116 | const { ReactiveInsights } = yield* bound.searchInsights({ |
| 117 | Type: "REACTIVE", |
| 118 | StartTimeRange: { |
| 119 | FromTime: new Date(Date.now() - 24 * 3600_000), |
| 120 | ToTime: new Date(), |
| 121 | }, |
| 122 | }); |
| 123 | return yield* HttpServerResponse.json({ |
| 124 | count: (ReactiveInsights ?? []).length, |
| 125 | }); |
| 126 | } |
| 127 | |
| 128 | // Per-stack health of the analyzed CloudFormation coverage. |
| 129 | if (request.method === "GET" && pathname === "/collection-health") { |
| 130 | const page = yield* bound.describeResourceCollectionHealth({ |
| 131 | ResourceCollectionType: "AWS_CLOUD_FORMATION", |
| 132 | }); |
| 133 | return yield* HttpServerResponse.json({ |
| 134 | stacks: (page.CloudFormation ?? []).length, |
| 135 | }); |
| 136 | } |
| 137 | |
| 138 | // Monitored resources — a typed not-found means DevOps Guru has no |
| 139 | // resource collection configured; report zero coverage. |
| 140 | if (request.method === "GET" && pathname === "/monitored") { |
| 141 | const count = yield* bound.listMonitoredResources().pipe( |
| 142 | Effect.map( |
| 143 | ({ MonitoredResourceIdentifiers }) => |
| 144 | MonitoredResourceIdentifiers.length, |
| 145 | ), |
| 146 | Effect.catchTag("ResourceNotFoundException", () => |
| 147 | Effect.succeed(0), |
| 148 | ), |
| 149 | ); |
| 150 | return yield* HttpServerResponse.json({ count }); |
| 151 | } |
| 152 | |
| 153 | return yield* HttpServerResponse.json( |
| 154 | { error: "Not found", method: request.method, pathname }, |
| 155 | { status: 404 }, |
| 156 | ); |
| 157 | }).pipe(Effect.orDie), |
| 158 | }; |
| 159 | }).pipe( |
| 160 | Effect.provide( |
| 161 | Layer.mergeAll( |
| 162 | Lambda.EventSource, |
| 163 | DevOpsGuru.DescribeAccountHealthHttp, |
| 164 | DevOpsGuru.DescribeAccountOverviewHttp, |
| 165 | DevOpsGuru.DescribeAnomalyHttp, |
| 166 | DevOpsGuru.DescribeFeedbackHttp, |
| 167 | DevOpsGuru.DescribeInsightHttp, |
| 168 | DevOpsGuru.DescribeOrganizationHealthHttp, |
| 169 | DevOpsGuru.DescribeOrganizationOverviewHttp, |
| 170 | DevOpsGuru.DescribeOrganizationResourceCollectionHealthHttp, |
| 171 | DevOpsGuru.DescribeResourceCollectionHealthHttp, |
| 172 | DevOpsGuru.GetCostEstimationHttp, |
| 173 | DevOpsGuru.StartCostEstimationHttp, |
| 174 | DevOpsGuru.ListAnomaliesForInsightHttp, |
| 175 | DevOpsGuru.ListAnomalousLogGroupsHttp, |
| 176 | DevOpsGuru.ListEventsHttp, |
| 177 | DevOpsGuru.ListInsightsHttp, |
| 178 | DevOpsGuru.ListMonitoredResourcesHttp, |
| 179 | DevOpsGuru.ListOrganizationInsightsHttp, |
| 180 | DevOpsGuru.ListRecommendationsHttp, |
| 181 | DevOpsGuru.PutFeedbackHttp, |
| 182 | DevOpsGuru.SearchInsightsHttp, |
| 183 | DevOpsGuru.SearchOrganizationInsightsHttp, |
| 184 | DevOpsGuru.DeleteInsightHttp, |
| 185 | ), |
| 186 | ), |
| 187 | ), |
| 188 | ); |