alchemy 2.0.0-beta.18 → 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 (128) 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/Agent/lsp/client.d.ts +2 -2
  13. package/lib/Apply.d.ts.map +1 -1
  14. package/lib/Apply.js +41 -9
  15. package/lib/Apply.js.map +1 -1
  16. package/lib/Axiom/Annotation.d.ts +49 -0
  17. package/lib/Axiom/Annotation.d.ts.map +1 -0
  18. package/lib/Axiom/Annotation.js +74 -0
  19. package/lib/Axiom/Annotation.js.map +1 -0
  20. package/lib/Axiom/ApiToken.d.ts +62 -0
  21. package/lib/Axiom/ApiToken.d.ts.map +1 -0
  22. package/lib/Axiom/ApiToken.js +98 -0
  23. package/lib/Axiom/ApiToken.js.map +1 -0
  24. package/lib/Axiom/AuthProvider.d.ts +48 -0
  25. package/lib/Axiom/AuthProvider.d.ts.map +1 -0
  26. package/lib/Axiom/AuthProvider.js +177 -0
  27. package/lib/Axiom/AuthProvider.js.map +1 -0
  28. package/lib/Axiom/Chart.d.ts +72 -0
  29. package/lib/Axiom/Chart.d.ts.map +1 -0
  30. package/lib/Axiom/Chart.js +14 -0
  31. package/lib/Axiom/Chart.js.map +1 -0
  32. package/lib/Axiom/Credentials.d.ts +12 -0
  33. package/lib/Axiom/Credentials.d.ts.map +1 -0
  34. package/lib/Axiom/Credentials.js +34 -0
  35. package/lib/Axiom/Credentials.js.map +1 -0
  36. package/lib/Axiom/Dashboard.d.ts +124 -0
  37. package/lib/Axiom/Dashboard.d.ts.map +1 -0
  38. package/lib/Axiom/Dashboard.js +140 -0
  39. package/lib/Axiom/Dashboard.js.map +1 -0
  40. package/lib/Axiom/Dataset.d.ts +101 -0
  41. package/lib/Axiom/Dataset.d.ts.map +1 -0
  42. package/lib/Axiom/Dataset.js +183 -0
  43. package/lib/Axiom/Dataset.js.map +1 -0
  44. package/lib/Axiom/Monitor.d.ts +74 -0
  45. package/lib/Axiom/Monitor.d.ts.map +1 -0
  46. package/lib/Axiom/Monitor.js +103 -0
  47. package/lib/Axiom/Monitor.js.map +1 -0
  48. package/lib/Axiom/Notifier.d.ts +63 -0
  49. package/lib/Axiom/Notifier.d.ts.map +1 -0
  50. package/lib/Axiom/Notifier.js +83 -0
  51. package/lib/Axiom/Notifier.js.map +1 -0
  52. package/lib/Axiom/Providers.d.ts +15 -0
  53. package/lib/Axiom/Providers.d.ts.map +1 -0
  54. package/lib/Axiom/Providers.js +32 -0
  55. package/lib/Axiom/Providers.js.map +1 -0
  56. package/lib/Axiom/View.d.ts +55 -0
  57. package/lib/Axiom/View.d.ts.map +1 -0
  58. package/lib/Axiom/View.js +79 -0
  59. package/lib/Axiom/View.js.map +1 -0
  60. package/lib/Axiom/VirtualField.d.ts +42 -0
  61. package/lib/Axiom/VirtualField.d.ts.map +1 -0
  62. package/lib/Axiom/VirtualField.js +71 -0
  63. package/lib/Axiom/VirtualField.js.map +1 -0
  64. package/lib/Axiom/index.d.ts +13 -0
  65. package/lib/Axiom/index.d.ts.map +1 -0
  66. package/lib/Axiom/index.js +13 -0
  67. package/lib/Axiom/index.js.map +1 -0
  68. package/lib/Bundle/Bundle.d.ts +20 -2
  69. package/lib/Bundle/Bundle.d.ts.map +1 -1
  70. package/lib/Bundle/Bundle.js +18 -3
  71. package/lib/Bundle/Bundle.js.map +1 -1
  72. package/lib/Bundle/PurePlugin.d.ts +109 -0
  73. package/lib/Bundle/PurePlugin.d.ts.map +1 -0
  74. package/lib/Bundle/PurePlugin.js +469 -0
  75. package/lib/Bundle/PurePlugin.js.map +1 -0
  76. package/lib/Bundle/index.d.ts +1 -0
  77. package/lib/Bundle/index.d.ts.map +1 -1
  78. package/lib/Bundle/index.js +1 -0
  79. package/lib/Bundle/index.js.map +1 -1
  80. package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
  81. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  82. package/lib/Cloudflare/Workers/Worker.js +19 -4
  83. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  84. package/lib/Output.d.ts.map +1 -1
  85. package/lib/Output.js +1 -1
  86. package/lib/Output.js.map +1 -1
  87. package/lib/Plan.d.ts.map +1 -1
  88. package/lib/Plan.js +10 -4
  89. package/lib/Plan.js.map +1 -1
  90. package/lib/Resource.d.ts +2 -2
  91. package/lib/Resource.d.ts.map +1 -1
  92. package/lib/Telemetry/Attributes.d.ts +43 -0
  93. package/lib/Telemetry/Attributes.d.ts.map +1 -0
  94. package/lib/Telemetry/Attributes.js +144 -0
  95. package/lib/Telemetry/Attributes.js.map +1 -0
  96. package/lib/Telemetry/Layer.d.ts +14 -0
  97. package/lib/Telemetry/Layer.d.ts.map +1 -0
  98. package/lib/Telemetry/Layer.js +50 -0
  99. package/lib/Telemetry/Layer.js.map +1 -0
  100. package/lib/Telemetry/Metrics.d.ts +34 -0
  101. package/lib/Telemetry/Metrics.d.ts.map +1 -0
  102. package/lib/Telemetry/Metrics.js +57 -0
  103. package/lib/Telemetry/Metrics.js.map +1 -0
  104. package/package.json +12 -1
  105. package/src/Apply.ts +118 -9
  106. package/src/Axiom/Annotation.ts +95 -0
  107. package/src/Axiom/ApiToken.ts +125 -0
  108. package/src/Axiom/AuthProvider.ts +341 -0
  109. package/src/Axiom/Chart.ts +94 -0
  110. package/src/Axiom/Credentials.ts +70 -0
  111. package/src/Axiom/Dashboard.ts +192 -0
  112. package/src/Axiom/Dataset.ts +291 -0
  113. package/src/Axiom/Monitor.ts +122 -0
  114. package/src/Axiom/Notifier.ts +104 -0
  115. package/src/Axiom/Providers.ts +58 -0
  116. package/src/Axiom/View.ts +106 -0
  117. package/src/Axiom/VirtualField.ts +90 -0
  118. package/src/Axiom/index.ts +12 -0
  119. package/src/Bundle/Bundle.ts +39 -1
  120. package/src/Bundle/PurePlugin.ts +556 -0
  121. package/src/Bundle/index.ts +1 -0
  122. package/src/Cloudflare/Workers/Worker.ts +37 -6
  123. package/src/Output.ts +2 -1
  124. package/src/Plan.ts +13 -4
  125. package/src/Resource.ts +2 -2
  126. package/src/Telemetry/Attributes.ts +216 -0
  127. package/src/Telemetry/Layer.ts +65 -0
  128. package/src/Telemetry/Metrics.ts +101 -0
@@ -0,0 +1,70 @@
1
+ import {
2
+ Credentials,
3
+ DEFAULT_API_BASE_URL,
4
+ } from "@distilled.cloud/axiom/Credentials";
5
+ import { ConfigError } from "@distilled.cloud/core/errors";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Layer from "effect/Layer";
8
+ import * as Match from "effect/Match";
9
+ import * as Redacted from "effect/Redacted";
10
+ import * as Config from "effect/Config";
11
+ import { getAuthProvider } from "../Auth/AuthProvider.ts";
12
+ import { ALCHEMY_PROFILE, loadOrConfigure } from "../Auth/Profile.ts";
13
+ import {
14
+ AXIOM_AUTH_PROVIDER_NAME,
15
+ type AxiomAuthConfig,
16
+ type AxiomResolvedCredentials,
17
+ } from "./AuthProvider.ts";
18
+
19
+ export {
20
+ Credentials,
21
+ CredentialsFromEnv,
22
+ DEFAULT_API_BASE_URL,
23
+ } from "@distilled.cloud/axiom/Credentials";
24
+
25
+ /**
26
+ * Build a `Credentials` layer that resolves Axiom credentials via the Alchemy
27
+ * AuthProvider using the configured profile (defaults to "default", overridable
28
+ * with the `ALCHEMY_PROFILE` env/config value).
29
+ */
30
+ export const fromAuthProvider = () =>
31
+ Layer.effect(
32
+ Credentials,
33
+ Effect.gen(function* () {
34
+ const auth = yield* getAuthProvider<
35
+ AxiomAuthConfig,
36
+ AxiomResolvedCredentials
37
+ >(AXIOM_AUTH_PROVIDER_NAME);
38
+ const profileName = yield* ALCHEMY_PROFILE;
39
+ const ci = yield* Config.boolean("CI").pipe(Config.withDefault(false));
40
+ const ctx = yield* Effect.context<never>();
41
+
42
+ return yield* loadOrConfigure(auth, profileName, { ci }).pipe(
43
+ Effect.flatMap((config) =>
44
+ auth.read(profileName, config as AxiomAuthConfig),
45
+ ),
46
+ Effect.map((creds) =>
47
+ Match.value(creds).pipe(
48
+ Match.when({ type: "apiToken" }, (c) => ({
49
+ apiKey: c.apiToken,
50
+ apiBaseUrl: c.apiBaseUrl,
51
+ orgId: c.orgId,
52
+ })),
53
+ Match.when({ type: "pat" }, (c) => ({
54
+ apiKey: c.apiToken,
55
+ apiBaseUrl: c.apiBaseUrl,
56
+ orgId: c.orgId,
57
+ })),
58
+ Match.exhaustive,
59
+ ),
60
+ ),
61
+ Effect.mapError(
62
+ (e) =>
63
+ new ConfigError({
64
+ message: `Failed to resolve Axiom credentials for profile '${profileName}': ${(e as { message?: string }).message ?? String(e)}`,
65
+ }),
66
+ ),
67
+ Effect.provide(ctx),
68
+ );
69
+ }),
70
+ );
@@ -0,0 +1,192 @@
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 { Chart, LayoutCell } from "./Chart.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ type DashboardDoc = Operations.CreateDashboardInput["dashboard"];
9
+
10
+ /**
11
+ * Dashboard input. Mirrors `Operations.CreateDashboardInput` but
12
+ * narrows `dashboard.charts` and `dashboard.layout` to the typed
13
+ * {@link Chart} / {@link LayoutCell} shapes (Axiom declares them
14
+ * as `Schema.Array(Schema.Unknown)`, so this is a compile-time-only
15
+ * refinement; runtime validation is unchanged).
16
+ */
17
+ export type DashboardProps = Omit<
18
+ Operations.CreateDashboardInput,
19
+ "dashboard"
20
+ > & {
21
+ readonly dashboard: Omit<DashboardDoc, "charts" | "layout"> & {
22
+ readonly charts: readonly Chart[];
23
+ readonly layout: readonly LayoutCell[];
24
+ };
25
+ };
26
+
27
+ export type Dashboard = Resource<
28
+ "Axiom.Dashboard",
29
+ DashboardProps,
30
+ {
31
+ /** Stable Axiom dashboard `uid` (used as the path identifier). */
32
+ uid: string;
33
+ id: string;
34
+ createdAt: string;
35
+ createdBy: string;
36
+ updatedAt: string;
37
+ updatedBy: string;
38
+ /** The full dashboard document as returned by Axiom. */
39
+ dashboard: Operations.CreateDashboardOutput["dashboard"]["dashboard"];
40
+ },
41
+ never,
42
+ Providers
43
+ >;
44
+
45
+ /**
46
+ * An Axiom dashboard — a named, layout-driven collection of charts. Each
47
+ * dashboard takes a full document (`charts` + `layout` array of grid cells +
48
+ * `timeWindow` + `refreshTime`) at version `schemaVersion: 2`.
49
+ *
50
+ * Charts are typed via {@link Chart} (a discriminated union over the
51
+ * Axiom-validated chart kinds — `TimeSeries`, `Table`, `Pie`, `Statistic`,
52
+ * `Heatmap`, `LogStream`, `Note`). The `id` on each chart is a free-form
53
+ * string the author picks; layout cells join via `LayoutCell.i`.
54
+ *
55
+ * The path identifier is `uid` (auto-assigned by Axiom). `id` is also
56
+ * exposed as an output but the API uses `uid` everywhere.
57
+ *
58
+ * Notes from probing `POST /v2/dashboards`:
59
+ *
60
+ * - Relative time windows must use the `qr-now-{duration}` form (e.g.
61
+ * `"qr-now-7d"` / `"qr-now"`); plain `"now-7d"` is rejected.
62
+ * - When authenticating with an API token, `dashboard.owner` must be `""`
63
+ * (Axiom rewrites this to the org-shared `X-AXIOM-EVERYONE`); per-user
64
+ * "private" dashboards aren't allowed for tokens.
65
+ * - The chart payload is strict: only `id`, `name`, `type`, `query`. Extra
66
+ * keys (e.g. `dataset`, `description`) trigger
67
+ * `Unrecognized keys: "<name>"`.
68
+ *
69
+ * @see https://axiom.co/docs/query-data/dashboards
70
+ *
71
+ * @section Creating a Dashboard
72
+ * @example Minimal empty dashboard
73
+ * ```typescript
74
+ * yield* Axiom.Dashboard("ops", {
75
+ * dashboard: {
76
+ * name: "Ops Overview",
77
+ * owner: "", // org-shared (required for API tokens)
78
+ * description: "Top-level service health",
79
+ * charts: [],
80
+ * layout: [],
81
+ * refreshTime: 60, // seconds: 15 | 60 | 300
82
+ * schemaVersion: 2,
83
+ * timeWindowStart: "qr-now-1h",
84
+ * timeWindowEnd: "qr-now",
85
+ * },
86
+ * });
87
+ * ```
88
+ *
89
+ * @example One-chart dashboard
90
+ * ```typescript
91
+ * import type { Chart, LayoutCell } from "alchemy/Axiom";
92
+ *
93
+ * const errors: Chart = {
94
+ * id: "errors-5m",
95
+ * name: "5xx errors / 5m",
96
+ * type: "TimeSeries",
97
+ * query: {
98
+ * apl: `['my-app-traces']
99
+ * | where status >= 500
100
+ * | summarize count() by bin_auto(_time)`,
101
+ * },
102
+ * };
103
+ *
104
+ * yield* Axiom.Dashboard("errors", {
105
+ * dashboard: {
106
+ * name: "Errors",
107
+ * owner: "",
108
+ * refreshTime: 60,
109
+ * schemaVersion: 2,
110
+ * timeWindowStart: "qr-now-24h",
111
+ * timeWindowEnd: "qr-now",
112
+ * charts: [errors],
113
+ * layout: [{ i: errors.id, x: 0, y: 0, w: 12, h: 6 } satisfies LayoutCell],
114
+ * },
115
+ * });
116
+ * ```
117
+ *
118
+ * @example Compare to last 24h
119
+ * ```typescript
120
+ * yield* Axiom.Dashboard("compare", {
121
+ * dashboard: {
122
+ * name: "Compare vs yesterday",
123
+ * owner: "",
124
+ * refreshTime: 300,
125
+ * schemaVersion: 2,
126
+ * timeWindowStart: "qr-now-1h",
127
+ * timeWindowEnd: "qr-now",
128
+ * against: "-1d", // overlay the same window from 24h ago
129
+ * charts: [],
130
+ * layout: [],
131
+ * },
132
+ * });
133
+ * ```
134
+ */
135
+ export const Dashboard = Resource<Dashboard>("Axiom.Dashboard");
136
+
137
+ export const DashboardProvider = () =>
138
+ Provider.effect(
139
+ Dashboard,
140
+ Effect.gen(function* () {
141
+ const create = yield* Operations.createDashboard;
142
+ const update = yield* Operations.updateDashboard;
143
+ const get = yield* Operations.getDashboard;
144
+ const del = yield* Operations.deleteDashboard;
145
+
146
+ const toAttrsFromCreate = (envelope: Operations.CreateDashboardOutput) => ({
147
+ uid: envelope.dashboard.uid,
148
+ id: envelope.dashboard.id,
149
+ createdAt: envelope.dashboard.createdAt,
150
+ createdBy: envelope.dashboard.createdBy,
151
+ updatedAt: envelope.dashboard.updatedAt,
152
+ updatedBy: envelope.dashboard.updatedBy,
153
+ dashboard: envelope.dashboard.dashboard,
154
+ });
155
+ const toAttrsFromGet = (current: Operations.GetDashboardOutput) => ({
156
+ uid: current.uid,
157
+ id: current.id,
158
+ createdAt: current.createdAt,
159
+ createdBy: current.createdBy,
160
+ updatedAt: current.updatedAt,
161
+ updatedBy: current.updatedBy,
162
+ dashboard: current.dashboard,
163
+ });
164
+
165
+ return {
166
+ stables: ["uid", "id", "createdAt", "createdBy"],
167
+ create: Effect.fn(function* ({ news }) {
168
+ return toAttrsFromCreate(yield* create(news));
169
+ }),
170
+ update: Effect.fn(function* ({ news, output }) {
171
+ // `overwrite: true` short-circuits Axiom's optimistic-concurrency
172
+ // check (otherwise the API requires the caller to echo back the
173
+ // server-side `version`, which the resource state doesn't track).
174
+ return toAttrsFromCreate(
175
+ yield* update({ ...news, uid: output.uid, overwrite: true }),
176
+ );
177
+ }),
178
+ delete: Effect.fn(function* ({ output }) {
179
+ yield* del({ uid: output.uid }).pipe(
180
+ Effect.catchTag("NotFound", () => Effect.void),
181
+ );
182
+ }),
183
+ read: Effect.fn(function* ({ output }) {
184
+ if (!output?.uid) return undefined;
185
+ return yield* get({ uid: output.uid }).pipe(
186
+ Effect.map(toAttrsFromGet),
187
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
188
+ );
189
+ }),
190
+ };
191
+ }),
192
+ );
@@ -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
+ );