alchemy 2.0.0-beta.17 → 2.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/bin/alchemy.js +428 -80
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/commands/_shared.ts +34 -0
  5. package/bin/commands/bootstrap.ts +10 -3
  6. package/bin/commands/deploy.ts +23 -3
  7. package/bin/commands/login.ts +18 -3
  8. package/bin/commands/logs.ts +11 -2
  9. package/bin/commands/profile.ts +11 -5
  10. package/bin/commands/state.ts +21 -13
  11. package/bin/commands/tail.ts +10 -2
  12. package/lib/AWS/CloudFront/CachePolicy.d.ts +106 -0
  13. package/lib/AWS/CloudFront/CachePolicy.d.ts.map +1 -0
  14. package/lib/AWS/CloudFront/CachePolicy.js +147 -0
  15. package/lib/AWS/CloudFront/CachePolicy.js.map +1 -0
  16. package/lib/AWS/CloudFront/KeyGroup.d.ts +72 -0
  17. package/lib/AWS/CloudFront/KeyGroup.d.ts.map +1 -0
  18. package/lib/AWS/CloudFront/KeyGroup.js +136 -0
  19. package/lib/AWS/CloudFront/KeyGroup.js.map +1 -0
  20. package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts +84 -0
  21. package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts.map +1 -0
  22. package/lib/AWS/CloudFront/OriginRequestPolicy.js +139 -0
  23. package/lib/AWS/CloudFront/OriginRequestPolicy.js.map +1 -0
  24. package/lib/AWS/CloudFront/PublicKey.d.ts +72 -0
  25. package/lib/AWS/CloudFront/PublicKey.d.ts.map +1 -0
  26. package/lib/AWS/CloudFront/PublicKey.js +137 -0
  27. package/lib/AWS/CloudFront/PublicKey.js.map +1 -0
  28. package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts +114 -0
  29. package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts.map +1 -0
  30. package/lib/AWS/CloudFront/ResponseHeadersPolicy.js +162 -0
  31. package/lib/AWS/CloudFront/ResponseHeadersPolicy.js.map +1 -0
  32. package/lib/AWS/CloudFront/index.d.ts +5 -0
  33. package/lib/AWS/CloudFront/index.d.ts.map +1 -1
  34. package/lib/AWS/CloudFront/index.js +5 -0
  35. package/lib/AWS/CloudFront/index.js.map +1 -1
  36. package/lib/AWS/ECS/CapacityProvider.d.ts +102 -0
  37. package/lib/AWS/ECS/CapacityProvider.d.ts.map +1 -0
  38. package/lib/AWS/ECS/CapacityProvider.js +217 -0
  39. package/lib/AWS/ECS/CapacityProvider.js.map +1 -0
  40. package/lib/AWS/ECS/index.d.ts +1 -0
  41. package/lib/AWS/ECS/index.d.ts.map +1 -1
  42. package/lib/AWS/ECS/index.js +1 -0
  43. package/lib/AWS/ECS/index.js.map +1 -1
  44. package/lib/AWS/Providers.d.ts.map +1 -1
  45. package/lib/AWS/Providers.js +7 -1
  46. package/lib/AWS/Providers.js.map +1 -1
  47. package/lib/Agent/lsp/client.d.ts +2 -2
  48. package/lib/Apply.d.ts.map +1 -1
  49. package/lib/Apply.js +41 -9
  50. package/lib/Apply.js.map +1 -1
  51. package/lib/Axiom/Annotation.d.ts +49 -0
  52. package/lib/Axiom/Annotation.d.ts.map +1 -0
  53. package/lib/Axiom/Annotation.js +74 -0
  54. package/lib/Axiom/Annotation.js.map +1 -0
  55. package/lib/Axiom/ApiToken.d.ts +62 -0
  56. package/lib/Axiom/ApiToken.d.ts.map +1 -0
  57. package/lib/Axiom/ApiToken.js +98 -0
  58. package/lib/Axiom/ApiToken.js.map +1 -0
  59. package/lib/Axiom/AuthProvider.d.ts +48 -0
  60. package/lib/Axiom/AuthProvider.d.ts.map +1 -0
  61. package/lib/Axiom/AuthProvider.js +177 -0
  62. package/lib/Axiom/AuthProvider.js.map +1 -0
  63. package/lib/Axiom/Chart.d.ts +72 -0
  64. package/lib/Axiom/Chart.d.ts.map +1 -0
  65. package/lib/Axiom/Chart.js +14 -0
  66. package/lib/Axiom/Chart.js.map +1 -0
  67. package/lib/Axiom/Credentials.d.ts +12 -0
  68. package/lib/Axiom/Credentials.d.ts.map +1 -0
  69. package/lib/Axiom/Credentials.js +34 -0
  70. package/lib/Axiom/Credentials.js.map +1 -0
  71. package/lib/Axiom/Dashboard.d.ts +124 -0
  72. package/lib/Axiom/Dashboard.d.ts.map +1 -0
  73. package/lib/Axiom/Dashboard.js +140 -0
  74. package/lib/Axiom/Dashboard.js.map +1 -0
  75. package/lib/Axiom/Dataset.d.ts +101 -0
  76. package/lib/Axiom/Dataset.d.ts.map +1 -0
  77. package/lib/Axiom/Dataset.js +183 -0
  78. package/lib/Axiom/Dataset.js.map +1 -0
  79. package/lib/Axiom/Monitor.d.ts +74 -0
  80. package/lib/Axiom/Monitor.d.ts.map +1 -0
  81. package/lib/Axiom/Monitor.js +103 -0
  82. package/lib/Axiom/Monitor.js.map +1 -0
  83. package/lib/Axiom/Notifier.d.ts +63 -0
  84. package/lib/Axiom/Notifier.d.ts.map +1 -0
  85. package/lib/Axiom/Notifier.js +83 -0
  86. package/lib/Axiom/Notifier.js.map +1 -0
  87. package/lib/Axiom/Providers.d.ts +15 -0
  88. package/lib/Axiom/Providers.d.ts.map +1 -0
  89. package/lib/Axiom/Providers.js +32 -0
  90. package/lib/Axiom/Providers.js.map +1 -0
  91. package/lib/Axiom/View.d.ts +55 -0
  92. package/lib/Axiom/View.d.ts.map +1 -0
  93. package/lib/Axiom/View.js +79 -0
  94. package/lib/Axiom/View.js.map +1 -0
  95. package/lib/Axiom/VirtualField.d.ts +42 -0
  96. package/lib/Axiom/VirtualField.d.ts.map +1 -0
  97. package/lib/Axiom/VirtualField.js +71 -0
  98. package/lib/Axiom/VirtualField.js.map +1 -0
  99. package/lib/Axiom/index.d.ts +13 -0
  100. package/lib/Axiom/index.d.ts.map +1 -0
  101. package/lib/Axiom/index.js +13 -0
  102. package/lib/Axiom/index.js.map +1 -0
  103. package/lib/Bundle/Bundle.d.ts +20 -2
  104. package/lib/Bundle/Bundle.d.ts.map +1 -1
  105. package/lib/Bundle/Bundle.js +18 -3
  106. package/lib/Bundle/Bundle.js.map +1 -1
  107. package/lib/Bundle/PurePlugin.d.ts +109 -0
  108. package/lib/Bundle/PurePlugin.d.ts.map +1 -0
  109. package/lib/Bundle/PurePlugin.js +469 -0
  110. package/lib/Bundle/PurePlugin.js.map +1 -0
  111. package/lib/Bundle/index.d.ts +1 -0
  112. package/lib/Bundle/index.d.ts.map +1 -1
  113. package/lib/Bundle/index.js +1 -0
  114. package/lib/Bundle/index.js.map +1 -1
  115. package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
  116. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  117. package/lib/Cloudflare/Workers/Worker.js +19 -4
  118. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  119. package/lib/GitHub/Secrets.d.ts +45 -0
  120. package/lib/GitHub/Secrets.d.ts.map +1 -0
  121. package/lib/GitHub/Secrets.js +30 -0
  122. package/lib/GitHub/Secrets.js.map +1 -0
  123. package/lib/GitHub/index.d.ts +1 -0
  124. package/lib/GitHub/index.d.ts.map +1 -1
  125. package/lib/GitHub/index.js +1 -0
  126. package/lib/GitHub/index.js.map +1 -1
  127. package/lib/Output.d.ts.map +1 -1
  128. package/lib/Output.js +1 -1
  129. package/lib/Output.js.map +1 -1
  130. package/lib/Plan.d.ts.map +1 -1
  131. package/lib/Plan.js +10 -4
  132. package/lib/Plan.js.map +1 -1
  133. package/lib/Resource.d.ts +2 -2
  134. package/lib/Resource.d.ts.map +1 -1
  135. package/lib/Telemetry/Attributes.d.ts +43 -0
  136. package/lib/Telemetry/Attributes.d.ts.map +1 -0
  137. package/lib/Telemetry/Attributes.js +144 -0
  138. package/lib/Telemetry/Attributes.js.map +1 -0
  139. package/lib/Telemetry/Layer.d.ts +14 -0
  140. package/lib/Telemetry/Layer.d.ts.map +1 -0
  141. package/lib/Telemetry/Layer.js +50 -0
  142. package/lib/Telemetry/Layer.js.map +1 -0
  143. package/lib/Telemetry/Metrics.d.ts +34 -0
  144. package/lib/Telemetry/Metrics.d.ts.map +1 -0
  145. package/lib/Telemetry/Metrics.js +57 -0
  146. package/lib/Telemetry/Metrics.js.map +1 -0
  147. package/package.json +12 -1
  148. package/src/AWS/CloudFront/CachePolicy.ts +271 -0
  149. package/src/AWS/CloudFront/KeyGroup.ts +233 -0
  150. package/src/AWS/CloudFront/OriginRequestPolicy.ts +256 -0
  151. package/src/AWS/CloudFront/PublicKey.ts +245 -0
  152. package/src/AWS/CloudFront/ResponseHeadersPolicy.ts +308 -0
  153. package/src/AWS/CloudFront/index.ts +11 -0
  154. package/src/AWS/ECS/CapacityProvider.ts +336 -0
  155. package/src/AWS/ECS/index.ts +4 -0
  156. package/src/AWS/Providers.ts +12 -0
  157. package/src/Apply.ts +118 -9
  158. package/src/Axiom/Annotation.ts +95 -0
  159. package/src/Axiom/ApiToken.ts +125 -0
  160. package/src/Axiom/AuthProvider.ts +341 -0
  161. package/src/Axiom/Chart.ts +94 -0
  162. package/src/Axiom/Credentials.ts +70 -0
  163. package/src/Axiom/Dashboard.ts +192 -0
  164. package/src/Axiom/Dataset.ts +291 -0
  165. package/src/Axiom/Monitor.ts +122 -0
  166. package/src/Axiom/Notifier.ts +104 -0
  167. package/src/Axiom/Providers.ts +58 -0
  168. package/src/Axiom/View.ts +106 -0
  169. package/src/Axiom/VirtualField.ts +90 -0
  170. package/src/Axiom/index.ts +12 -0
  171. package/src/Bundle/Bundle.ts +39 -1
  172. package/src/Bundle/PurePlugin.ts +556 -0
  173. package/src/Bundle/index.ts +1 -0
  174. package/src/Cloudflare/Workers/Worker.ts +37 -6
  175. package/src/GitHub/Secrets.ts +66 -0
  176. package/src/GitHub/index.ts +1 -0
  177. package/src/Output.ts +2 -1
  178. package/src/Plan.ts +13 -4
  179. package/src/Resource.ts +2 -2
  180. package/src/Telemetry/Attributes.ts +216 -0
  181. package/src/Telemetry/Layer.ts +65 -0
  182. package/src/Telemetry/Metrics.ts +101 -0
@@ -0,0 +1,291 @@
1
+ import { Credentials } from "@distilled.cloud/axiom/Credentials";
2
+ import * as Operations from "@distilled.cloud/axiom/Operations";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Option from "effect/Option";
5
+ import { AdoptPolicy } from "../AdoptPolicy.ts";
6
+ import { isResolved } from "../Diff.ts";
7
+ import * as Provider from "../Provider.ts";
8
+ import { Resource } from "../Resource.ts";
9
+ import { Stack } from "../Stack.ts";
10
+ import { Stage } from "../Stage.ts";
11
+ import type { Providers } from "./Providers.ts";
12
+
13
+ export type DatasetKind =
14
+ | "otel:metrics:v1"
15
+ | "otel:traces:v1"
16
+ | "otel:logs:v1"
17
+ | "axiom:events:v1";
18
+
19
+ export type DatasetProps = {
20
+ /**
21
+ * Dataset name. Used as the dataset's stable identifier in Axiom — changing
22
+ * this triggers a replacement.
23
+ */
24
+ name: string;
25
+ /** Free-form description shown in the Axiom UI. */
26
+ description?: string;
27
+ /**
28
+ * Dataset kind. Defaults to `axiom:events:v1`.
29
+ *
30
+ * For OTEL pipelines, choose:
31
+ * - `otel:traces:v1` for traces
32
+ * - `otel:logs:v1` for logs
33
+ * - `otel:metrics:v1` for metrics
34
+ *
35
+ * Cannot be changed after creation — triggers a replacement.
36
+ */
37
+ kind?: DatasetKind;
38
+ /** Retention in days. Plan-dependent — see Axiom docs. */
39
+ retentionDays?: number;
40
+ /** Whether to enforce the configured retention period. */
41
+ useRetentionPeriod?: boolean;
42
+ };
43
+
44
+ export type Dataset = Resource<
45
+ "Axiom.Dataset",
46
+ DatasetProps,
47
+ {
48
+ id: string;
49
+ name: string;
50
+ kind: DatasetKind;
51
+ description: string;
52
+ created: string;
53
+ apiBaseUrl: string;
54
+ /** Root OTLP endpoint (`${apiBaseUrl}`). Most exporters auto-append the signal path. */
55
+ otelEndpoint: string;
56
+ /** OTLP/HTTP traces endpoint. */
57
+ otelTracesEndpoint: string;
58
+ /** OTLP/HTTP logs endpoint. */
59
+ otelLogsEndpoint: string;
60
+ /** OTLP/HTTP metrics endpoint. */
61
+ otelMetricsEndpoint: string;
62
+ /**
63
+ * Headers required for OTLP shipping aside from the bearer token.
64
+ * Add `Authorization: Bearer <AXIOM_TOKEN>` separately at runtime so the
65
+ * secret is never persisted in resource state.
66
+ */
67
+ otelHeaders: Record<string, string>;
68
+ },
69
+ never,
70
+ Providers
71
+ >;
72
+
73
+ /**
74
+ * An Axiom dataset — the top-level container that stores events, logs,
75
+ * traces, or metrics. Pick a `kind` up-front: it determines schema and how
76
+ * the data is shown in the UI, and **cannot be changed** after creation
77
+ * (changing it triggers a replacement, which deletes the data).
78
+ *
79
+ * Datasets expose Axiom's OTLP/HTTP endpoints (`otelTracesEndpoint`,
80
+ * `otelLogsEndpoint`, `otelMetricsEndpoint`) as output attributes so you can
81
+ * inject them into a Worker / Lambda's env vars for OpenTelemetry shipping.
82
+ * The bearer token is **not** stored in resource state — supply
83
+ * `Authorization: Bearer <AXIOM_TOKEN>` separately at runtime.
84
+ *
85
+ * @see https://axiom.co/docs/reference/datasets
86
+ * @see https://axiom.co/docs/send-data/opentelemetry — OTLP endpoint reference
87
+ *
88
+ * @section Creating a Dataset
89
+ * @example Logs dataset with 30-day retention
90
+ * ```typescript
91
+ * const logs = yield* Axiom.Dataset("app-logs", {
92
+ * name: "my-app-logs",
93
+ * kind: "otel:logs:v1",
94
+ * description: "Application logs from prod workers",
95
+ * retentionDays: 30,
96
+ * useRetentionPeriod: true,
97
+ * });
98
+ * ```
99
+ *
100
+ * @example Separate datasets per OTEL signal
101
+ * ```typescript
102
+ * const traces = yield* Axiom.Dataset("traces", { name: "app-traces", kind: "otel:traces:v1" });
103
+ * const logs = yield* Axiom.Dataset("logs", { name: "app-logs", kind: "otel:logs:v1" });
104
+ * const metrics = yield* Axiom.Dataset("metrics", { name: "app-metrics", kind: "otel:metrics:v1" });
105
+ * ```
106
+ *
107
+ * @section Shipping OTEL data
108
+ * @example Wire OTEL env vars into a Cloudflare Worker
109
+ * ```typescript
110
+ * yield* Cloudflare.Worker("api", {
111
+ * vars: {
112
+ * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: traces.otelTracesEndpoint,
113
+ * OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: logs.otelLogsEndpoint,
114
+ * // Bearer token must come from a secret store, not the dataset state.
115
+ * OTEL_EXPORTER_OTLP_HEADERS:
116
+ * `Authorization=Bearer ${env.AXIOM_TOKEN},X-Axiom-Dataset=${traces.name}`,
117
+ * },
118
+ * });
119
+ * ```
120
+ */
121
+ export const Dataset = Resource<Dataset>("Axiom.Dataset");
122
+
123
+ /**
124
+ * Axiom has no tags/labels API. The only writable field we can use to mark
125
+ * ownership is `description`. We append a deterministic marker on create so
126
+ * that on a re-apply (e.g. state was wiped) we can safely identify a dataset
127
+ * that **we** previously created and adopt it idempotently — without
128
+ * accidentally hijacking a dataset created by someone else with the same name.
129
+ */
130
+ const MARKER_RE = /\s*\[alchemy:stack=([^;]+);stage=([^;]+);id=([^\]]+)\]\s*$/;
131
+
132
+ const buildMarker = (stack: string, stage: string, id: string) =>
133
+ `[alchemy:stack=${stack};stage=${stage};id=${id}]`;
134
+
135
+ const augmentDescription = (
136
+ description: string | undefined,
137
+ marker: string,
138
+ ) => {
139
+ const base = stripMarker(description ?? "");
140
+ return base.length > 0 ? `${base}\n${marker}` : marker;
141
+ };
142
+
143
+ const stripMarker = (description: string): string =>
144
+ description.replace(MARKER_RE, "").trimEnd();
145
+
146
+ const parseMarker = (
147
+ description: string | undefined,
148
+ ): { stack: string; stage: string; id: string } | undefined => {
149
+ if (!description) return undefined;
150
+ const m = description.match(MARKER_RE);
151
+ if (!m) return undefined;
152
+ return { stack: m[1], stage: m[2], id: m[3] };
153
+ };
154
+
155
+ const buildOtelAttrs = (apiBaseUrl: string, name: string) => {
156
+ const root = apiBaseUrl.replace(/\/$/, "");
157
+ return {
158
+ apiBaseUrl: root,
159
+ otelEndpoint: root,
160
+ otelTracesEndpoint: `${root}/v1/traces`,
161
+ otelLogsEndpoint: `${root}/v1/logs`,
162
+ otelMetricsEndpoint: `${root}/v1/metrics`,
163
+ otelHeaders: { "X-Axiom-Dataset": name } as Record<string, string>,
164
+ };
165
+ };
166
+
167
+ export const DatasetProvider = () =>
168
+ Provider.effect(
169
+ Dataset,
170
+ Effect.gen(function* () {
171
+ const { apiBaseUrl } = yield* Credentials;
172
+ const create = yield* Operations.createDataset;
173
+ const update = yield* Operations.updateDataset;
174
+ const get = yield* Operations.getDataset;
175
+ const del = yield* Operations.deleteDataset;
176
+
177
+ const toAttrs = (
178
+ dataset: Operations.CreateDatasetOutput,
179
+ ) => ({
180
+ id: dataset.id,
181
+ name: dataset.name,
182
+ kind: dataset.kind,
183
+ description: stripMarker(dataset.description),
184
+ created: dataset.created,
185
+ ...buildOtelAttrs(apiBaseUrl, dataset.name),
186
+ });
187
+
188
+ return {
189
+ stables: ["id", "name", "kind"],
190
+ diff: Effect.fn(function* ({ olds, news, output }) {
191
+ if (!isResolved(news)) return undefined;
192
+ if (output && news.name !== output.name) {
193
+ return { action: "replace" } as const;
194
+ }
195
+ if (news.kind && output && news.kind !== output.kind) {
196
+ return { action: "replace" } as const;
197
+ }
198
+ if (
199
+ news.description !== olds?.description ||
200
+ news.retentionDays !== olds?.retentionDays ||
201
+ news.useRetentionPeriod !== olds?.useRetentionPeriod
202
+ ) {
203
+ return { action: "update" } as const;
204
+ }
205
+ return undefined;
206
+ }),
207
+ create: Effect.fn(function* ({ id, news }) {
208
+ const stack = yield* Stack;
209
+ const stage = yield* Stage;
210
+ const adoptPolicy = yield* Effect.serviceOption(AdoptPolicy).pipe(
211
+ Effect.map(Option.getOrElse(() => false)),
212
+ );
213
+ const marker = buildMarker(stack.name, stage, id);
214
+ const dataset = yield* (
215
+ create({
216
+ name: news.name,
217
+ description: augmentDescription(news.description, marker),
218
+ kind: news.kind,
219
+ retentionDays: news.retentionDays,
220
+ useRetentionPeriod: news.useRetentionPeriod,
221
+ }) as Effect.Effect<
222
+ Operations.CreateDatasetOutput,
223
+ { readonly _tag: string },
224
+ never
225
+ >
226
+ ).pipe(
227
+ Effect.catchIf(
228
+ (
229
+ e,
230
+ ): e is { readonly _tag: "Conflict" | "UnprocessableEntity" } =>
231
+ e._tag === "Conflict" || e._tag === "UnprocessableEntity",
232
+ () =>
233
+ Effect.gen(function* () {
234
+ const existing = yield* get({ dataset_id: news.name });
235
+ const ownership = parseMarker(existing.description);
236
+ const isOurs =
237
+ ownership !== undefined &&
238
+ ownership.stack === stack.name &&
239
+ ownership.stage === stage &&
240
+ ownership.id === id;
241
+ if (isOurs || adoptPolicy) {
242
+ // Re-apply the user's desired props (and re-stamp marker)
243
+ // so adoption is idempotent and Axiom-side state matches
244
+ // the resource spec.
245
+ return yield* update({
246
+ dataset_id: existing.id,
247
+ description: augmentDescription(news.description, marker),
248
+ retentionDays: news.retentionDays,
249
+ useRetentionPeriod: news.useRetentionPeriod,
250
+ });
251
+ }
252
+ return yield* Effect.die(
253
+ new Error(
254
+ `Axiom dataset "${news.name}" already exists and was ` +
255
+ `not created by this stack (${stack.name}/${stage}/${id}). ` +
256
+ `Pipe the resource through \`adopt()\` from "alchemy/AdoptPolicy" ` +
257
+ `to take ownership, or delete the dataset in Axiom and retry.`,
258
+ ),
259
+ );
260
+ }),
261
+ ),
262
+ );
263
+ return toAttrs(dataset);
264
+ }),
265
+ update: Effect.fn(function* ({ id, news, output }) {
266
+ const stack = yield* Stack;
267
+ const stage = yield* Stage;
268
+ const marker = buildMarker(stack.name, stage, id);
269
+ const dataset = yield* update({
270
+ dataset_id: output.id,
271
+ description: augmentDescription(news.description, marker),
272
+ retentionDays: news.retentionDays,
273
+ useRetentionPeriod: news.useRetentionPeriod,
274
+ });
275
+ return toAttrs(dataset);
276
+ }),
277
+ delete: Effect.fn(function* ({ output }) {
278
+ yield* del({ dataset_id: output.id }).pipe(
279
+ Effect.catchTag("NotFound", () => Effect.void),
280
+ );
281
+ }),
282
+ read: Effect.fn(function* ({ output }) {
283
+ if (!output?.id) return undefined;
284
+ return yield* get({ dataset_id: output.id }).pipe(
285
+ Effect.map(toAttrs),
286
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
287
+ );
288
+ }),
289
+ };
290
+ }),
291
+ );
@@ -0,0 +1,122 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Resource } from "../Resource.ts";
5
+ import { isResolved } from "../Diff.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export type MonitorProps = Operations.CreateMonitorInput;
9
+
10
+ export type Monitor = Resource<
11
+ "Axiom.Monitor",
12
+ MonitorProps,
13
+ Operations.CreateMonitorOutput,
14
+ never,
15
+ Providers
16
+ >;
17
+
18
+ /**
19
+ * An Axiom monitor — a scheduled APL/MPL query that evaluates on a fixed
20
+ * cadence and fires alerts via {@link Notifier notifiers} when its
21
+ * condition is met.
22
+ *
23
+ * Three monitor `type`s are supported:
24
+ *
25
+ * - **`Threshold`** — fires when an aggregate result crosses a static
26
+ * `threshold` (compared with `operator`).
27
+ * - **`MatchEvent`** — fires for every event matching the query.
28
+ * - **`AnomalyDetection`** — fires when results deviate from a learned
29
+ * baseline by more than `tolerance` over `compareDays`.
30
+ *
31
+ * Changing `type` triggers a replacement; everything else updates in place.
32
+ *
33
+ * @see https://axiom.co/docs/monitor-data/monitors
34
+ *
35
+ * @section Creating a Monitor
36
+ * @example Threshold: alert on >100 errors per 5m
37
+ * ```typescript
38
+ * yield* Axiom.Monitor("error-rate", {
39
+ * name: "High error rate",
40
+ * description: "Fires when error count exceeds 100/5m",
41
+ * type: "Threshold",
42
+ * aplQuery: `
43
+ * ['my-app-traces']
44
+ * | where status >= 500
45
+ * | summarize count() by bin_auto(_time)
46
+ * `,
47
+ * operator: "Above",
48
+ * threshold: 100,
49
+ * intervalMinutes: 5,
50
+ * rangeMinutes: 5,
51
+ * alertOnNoData: false,
52
+ * resolvable: true,
53
+ * notifierIds: [slack.id, pagerduty.id],
54
+ * });
55
+ * ```
56
+ *
57
+ * @example MatchEvent: alert on every panic
58
+ * ```typescript
59
+ * yield* Axiom.Monitor("panics", {
60
+ * name: "Service panic",
61
+ * type: "MatchEvent",
62
+ * aplQuery: `['my-app-logs'] | where message contains "panic:"`,
63
+ * intervalMinutes: 1,
64
+ * rangeMinutes: 1,
65
+ * notifierIds: [pagerduty.id],
66
+ * });
67
+ * ```
68
+ *
69
+ * @example AnomalyDetection: deviation vs. last 7 days
70
+ * ```typescript
71
+ * yield* Axiom.Monitor("traffic-anomaly", {
72
+ * name: "Traffic anomaly",
73
+ * type: "AnomalyDetection",
74
+ * aplQuery: `['my-app-traces'] | summarize count() by bin_auto(_time)`,
75
+ * compareDays: 7,
76
+ * tolerance: 25, // %
77
+ * intervalMinutes: 15,
78
+ * rangeMinutes: 15,
79
+ * notifierIds: [slack.id],
80
+ * });
81
+ * ```
82
+ */
83
+ export const Monitor = Resource<Monitor>("Axiom.Monitor");
84
+
85
+ export const MonitorProvider = () =>
86
+ Provider.effect(
87
+ Monitor,
88
+ Effect.gen(function* () {
89
+ const create = yield* Operations.createMonitor;
90
+ const update = yield* Operations.updateMonitor;
91
+ const get = yield* Operations.getMonitor;
92
+ const del = yield* Operations.deleteMonitor;
93
+
94
+ return {
95
+ stables: ["id"],
96
+ diff: Effect.fn(function* ({ news, output }) {
97
+ if (!isResolved(news)) return undefined;
98
+ if (output && news.type !== output.type) {
99
+ return { action: "replace" } as const;
100
+ }
101
+ return undefined;
102
+ }),
103
+ create: Effect.fn(function* ({ news }) {
104
+ return yield* create(news);
105
+ }),
106
+ update: Effect.fn(function* ({ news, output }) {
107
+ return yield* update({ ...news, id: output.id });
108
+ }),
109
+ delete: Effect.fn(function* ({ output }) {
110
+ yield* del({ id: output.id }).pipe(
111
+ Effect.catchTag("NotFound", () => Effect.void),
112
+ );
113
+ }),
114
+ read: Effect.fn(function* ({ output }) {
115
+ if (!output?.id) return undefined;
116
+ return yield* get({ id: output.id }).pipe(
117
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
118
+ );
119
+ }),
120
+ };
121
+ }),
122
+ );
@@ -0,0 +1,104 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Resource } from "../Resource.ts";
5
+ import type { Providers } from "./Providers.ts";
6
+
7
+ export type NotifierProps = Operations.CreateNotifierInput;
8
+
9
+ export type Notifier = Resource<
10
+ "Axiom.Notifier",
11
+ NotifierProps,
12
+ Operations.CreateNotifierOutput & { id: string },
13
+ never,
14
+ Providers
15
+ >;
16
+
17
+ /**
18
+ * An Axiom notifier — an alert destination (Slack, email, PagerDuty,
19
+ * Opsgenie, Discord, Microsoft Teams, generic webhook, or a fully custom
20
+ * webhook with templated body/headers) that {@link Monitor monitors} target
21
+ * via `notifierIds`. Exactly one channel under `properties` should be set.
22
+ *
23
+ * @see https://axiom.co/docs/monitor-data/notifiers
24
+ *
25
+ * @section Creating a Notifier
26
+ * @example Slack incoming webhook
27
+ * ```typescript
28
+ * const slack = yield* Axiom.Notifier("ops-slack", {
29
+ * name: "ops-channel",
30
+ * properties: {
31
+ * slack: { slackUrl: process.env.SLACK_WEBHOOK_URL! },
32
+ * },
33
+ * });
34
+ * ```
35
+ *
36
+ * @example Email distribution list
37
+ * ```typescript
38
+ * yield* Axiom.Notifier("ops-email", {
39
+ * name: "ops-team",
40
+ * properties: { email: { emails: ["sre@example.com", "oncall@example.com"] } },
41
+ * });
42
+ * ```
43
+ *
44
+ * @example PagerDuty integration
45
+ * ```typescript
46
+ * yield* Axiom.Notifier("pagerduty", {
47
+ * name: "primary-oncall",
48
+ * properties: {
49
+ * pagerduty: { routingKey: process.env.PAGERDUTY_ROUTING_KEY!, token: "" },
50
+ * },
51
+ * });
52
+ * ```
53
+ *
54
+ * @example Custom webhook with templated body
55
+ * ```typescript
56
+ * yield* Axiom.Notifier("incident-webhook", {
57
+ * name: "incident.io",
58
+ * properties: {
59
+ * customWebhook: {
60
+ * url: "https://api.incident.io/v2/alert_events",
61
+ * headers: { "Content-Type": "application/json" },
62
+ * secretHeaders: { Authorization: `Bearer ${process.env.INCIDENT_TOKEN}` },
63
+ * body: '{"title": "{{.Monitor.Name}}", "status": "firing"}',
64
+ * },
65
+ * },
66
+ * });
67
+ * ```
68
+ */
69
+ export const Notifier = Resource<Notifier>("Axiom.Notifier");
70
+
71
+ export const NotifierProvider = () =>
72
+ Provider.effect(
73
+ Notifier,
74
+ Effect.gen(function* () {
75
+ const create = yield* Operations.createNotifier;
76
+ const update = yield* Operations.updateNotifier;
77
+ const get = yield* Operations.getNotifier;
78
+ const del = yield* Operations.deleteNotifier;
79
+
80
+ return {
81
+ stables: ["id"],
82
+ create: Effect.fn(function* ({ news }) {
83
+ const result = yield* create(news);
84
+ return { ...result, id: result.id ?? "" };
85
+ }),
86
+ update: Effect.fn(function* ({ news, output }) {
87
+ const result = yield* update({ ...news, id: output.id });
88
+ return { ...result, id: result.id ?? output.id };
89
+ }),
90
+ delete: Effect.fn(function* ({ output }) {
91
+ yield* del({ id: output.id }).pipe(
92
+ Effect.catchTag("NotFound", () => Effect.void),
93
+ );
94
+ }),
95
+ read: Effect.fn(function* ({ output }) {
96
+ if (!output?.id) return undefined;
97
+ return yield* get({ id: output.id }).pipe(
98
+ Effect.map((current) => ({ ...current, id: current.id ?? output.id })),
99
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
100
+ );
101
+ }),
102
+ };
103
+ }),
104
+ );
@@ -0,0 +1,58 @@
1
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
2
+ import * as Layer from "effect/Layer";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Annotation, AnnotationProvider } from "./Annotation.ts";
5
+ import { ApiToken, ApiTokenProvider } from "./ApiToken.ts";
6
+ import { AxiomAuth } from "./AuthProvider.ts";
7
+ import * as Credentials from "./Credentials.ts";
8
+ import { Dashboard, DashboardProvider } from "./Dashboard.ts";
9
+ import { Dataset, DatasetProvider } from "./Dataset.ts";
10
+ import { Monitor, MonitorProvider } from "./Monitor.ts";
11
+ import { Notifier, NotifierProvider } from "./Notifier.ts";
12
+ import { View, ViewProvider } from "./View.ts";
13
+ import { VirtualField, VirtualFieldProvider } from "./VirtualField.ts";
14
+
15
+ export { Credentials } from "@distilled.cloud/axiom/Credentials";
16
+
17
+ export class Providers extends Provider.ProviderCollection<Providers>()(
18
+ "Axiom",
19
+ ) {}
20
+
21
+ export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
22
+
23
+ /**
24
+ * Axiom providers and credentials. Wires up Dataset, ApiToken, Notifier,
25
+ * Monitor, Dashboard, VirtualField, View and Annotation resources, and
26
+ * registers the Axiom AuthProvider so `alchemy login` can configure it.
27
+ */
28
+ export const providers = () =>
29
+ Layer.effect(
30
+ Providers,
31
+ Provider.collection([
32
+ Annotation,
33
+ ApiToken,
34
+ Dashboard,
35
+ Dataset,
36
+ Monitor,
37
+ Notifier,
38
+ View,
39
+ VirtualField,
40
+ ]),
41
+ ).pipe(
42
+ Layer.provide(
43
+ Layer.mergeAll(
44
+ AnnotationProvider(),
45
+ ApiTokenProvider(),
46
+ DashboardProvider(),
47
+ DatasetProvider(),
48
+ MonitorProvider(),
49
+ NotifierProvider(),
50
+ ViewProvider(),
51
+ VirtualFieldProvider(),
52
+ ),
53
+ ),
54
+ Layer.provideMerge(Credentials.fromAuthProvider()),
55
+ Layer.provideMerge(FetchHttpClient.layer),
56
+ Layer.provideMerge(AxiomAuth),
57
+ Layer.orDie,
58
+ );
@@ -0,0 +1,106 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import { isResolved } from "../Diff.ts";
4
+ import * as Provider from "../Provider.ts";
5
+ import { Resource } from "../Resource.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export type ViewProps = Operations.CreateViewInput;
9
+
10
+ export type View = Resource<
11
+ "Axiom.View",
12
+ ViewProps,
13
+ Operations.CreateViewOutput & {
14
+ /**
15
+ * Path identifier used by `updateView` / `getView` / `deleteView`.
16
+ * Currently derived from `name` because Axiom's view list/get responses
17
+ * don't expose a separate id field.
18
+ */
19
+ id: string;
20
+ },
21
+ never,
22
+ Providers
23
+ >;
24
+
25
+ /**
26
+ * An Axiom saved view — a named, shareable APL query. Useful for building
27
+ * starter dashboards, providing canned "open in Axiom" links from your app,
28
+ * or pinning common investigations the team revisits.
29
+ *
30
+ * The path identifier is `name`. Renaming a view triggers a replacement
31
+ * (the old one is deleted, a new one is created).
32
+ *
33
+ * @see https://axiom.co/docs/query-data/datasets — APL query reference
34
+ *
35
+ * @section Creating a View
36
+ * @example Recent errors across one dataset
37
+ * ```typescript
38
+ * yield* Axiom.View("recent-errors", {
39
+ * name: "recent-errors",
40
+ * description: "Last 100 5xx responses",
41
+ * datasets: ["my-app-traces"],
42
+ * aplQuery: `
43
+ * ['my-app-traces']
44
+ * | where status >= 500
45
+ * | order by _time desc
46
+ * | take 100
47
+ * `,
48
+ * });
49
+ * ```
50
+ *
51
+ * @example Cross-dataset join (logs + traces by trace_id)
52
+ * ```typescript
53
+ * yield* Axiom.View("trace-with-logs", {
54
+ * name: "trace-with-logs",
55
+ * datasets: ["my-app-traces", "my-app-logs"],
56
+ * aplQuery: `
57
+ * ['my-app-traces']
58
+ * | where duration_ms > 1000
59
+ * | join kind=leftouter (['my-app-logs']) on trace_id
60
+ * `,
61
+ * });
62
+ * ```
63
+ */
64
+ export const View = Resource<View>("Axiom.View");
65
+
66
+ export const ViewProvider = () =>
67
+ Provider.effect(
68
+ View,
69
+ Effect.gen(function* () {
70
+ const create = yield* Operations.createView;
71
+ const update = yield* Operations.updateView;
72
+ const get = yield* Operations.getView;
73
+ const del = yield* Operations.deleteView;
74
+
75
+ return {
76
+ stables: ["id"],
77
+ diff: Effect.fn(function* ({ news, output }) {
78
+ if (!isResolved(news)) return undefined;
79
+ if (output && news.name !== output.name) {
80
+ return { action: "replace" } as const;
81
+ }
82
+ return undefined;
83
+ }),
84
+ create: Effect.fn(function* ({ news }) {
85
+ const result = yield* create(news);
86
+ return { ...result, id: news.name };
87
+ }),
88
+ update: Effect.fn(function* ({ news, output }) {
89
+ const result = yield* update({ ...news, id: output.id });
90
+ return { ...result, id: output.id };
91
+ }),
92
+ delete: Effect.fn(function* ({ output }) {
93
+ yield* del({ id: output.id }).pipe(
94
+ Effect.catchTag("NotFound", () => Effect.void),
95
+ );
96
+ }),
97
+ read: Effect.fn(function* ({ output }) {
98
+ if (!output?.id) return undefined;
99
+ return yield* get({ id: output.id }).pipe(
100
+ Effect.map((current) => ({ ...current, id: output.id })),
101
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
102
+ );
103
+ }),
104
+ };
105
+ }),
106
+ );