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.
- package/bin/alchemy.js +428 -80
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/commands/_shared.ts +34 -0
- package/bin/commands/bootstrap.ts +10 -3
- package/bin/commands/deploy.ts +23 -3
- package/bin/commands/login.ts +18 -3
- package/bin/commands/logs.ts +11 -2
- package/bin/commands/profile.ts +11 -5
- package/bin/commands/state.ts +21 -13
- package/bin/commands/tail.ts +10 -2
- package/lib/Agent/lsp/client.d.ts +2 -2
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +41 -9
- package/lib/Apply.js.map +1 -1
- package/lib/Axiom/Annotation.d.ts +49 -0
- package/lib/Axiom/Annotation.d.ts.map +1 -0
- package/lib/Axiom/Annotation.js +74 -0
- package/lib/Axiom/Annotation.js.map +1 -0
- package/lib/Axiom/ApiToken.d.ts +62 -0
- package/lib/Axiom/ApiToken.d.ts.map +1 -0
- package/lib/Axiom/ApiToken.js +98 -0
- package/lib/Axiom/ApiToken.js.map +1 -0
- package/lib/Axiom/AuthProvider.d.ts +48 -0
- package/lib/Axiom/AuthProvider.d.ts.map +1 -0
- package/lib/Axiom/AuthProvider.js +177 -0
- package/lib/Axiom/AuthProvider.js.map +1 -0
- package/lib/Axiom/Chart.d.ts +72 -0
- package/lib/Axiom/Chart.d.ts.map +1 -0
- package/lib/Axiom/Chart.js +14 -0
- package/lib/Axiom/Chart.js.map +1 -0
- package/lib/Axiom/Credentials.d.ts +12 -0
- package/lib/Axiom/Credentials.d.ts.map +1 -0
- package/lib/Axiom/Credentials.js +34 -0
- package/lib/Axiom/Credentials.js.map +1 -0
- package/lib/Axiom/Dashboard.d.ts +124 -0
- package/lib/Axiom/Dashboard.d.ts.map +1 -0
- package/lib/Axiom/Dashboard.js +140 -0
- package/lib/Axiom/Dashboard.js.map +1 -0
- package/lib/Axiom/Dataset.d.ts +101 -0
- package/lib/Axiom/Dataset.d.ts.map +1 -0
- package/lib/Axiom/Dataset.js +183 -0
- package/lib/Axiom/Dataset.js.map +1 -0
- package/lib/Axiom/Monitor.d.ts +74 -0
- package/lib/Axiom/Monitor.d.ts.map +1 -0
- package/lib/Axiom/Monitor.js +103 -0
- package/lib/Axiom/Monitor.js.map +1 -0
- package/lib/Axiom/Notifier.d.ts +63 -0
- package/lib/Axiom/Notifier.d.ts.map +1 -0
- package/lib/Axiom/Notifier.js +83 -0
- package/lib/Axiom/Notifier.js.map +1 -0
- package/lib/Axiom/Providers.d.ts +15 -0
- package/lib/Axiom/Providers.d.ts.map +1 -0
- package/lib/Axiom/Providers.js +32 -0
- package/lib/Axiom/Providers.js.map +1 -0
- package/lib/Axiom/View.d.ts +55 -0
- package/lib/Axiom/View.d.ts.map +1 -0
- package/lib/Axiom/View.js +79 -0
- package/lib/Axiom/View.js.map +1 -0
- package/lib/Axiom/VirtualField.d.ts +42 -0
- package/lib/Axiom/VirtualField.d.ts.map +1 -0
- package/lib/Axiom/VirtualField.js +71 -0
- package/lib/Axiom/VirtualField.js.map +1 -0
- package/lib/Axiom/index.d.ts +13 -0
- package/lib/Axiom/index.d.ts.map +1 -0
- package/lib/Axiom/index.js +13 -0
- package/lib/Axiom/index.js.map +1 -0
- package/lib/Bundle/Bundle.d.ts +20 -2
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +18 -3
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Bundle/PurePlugin.d.ts +109 -0
- package/lib/Bundle/PurePlugin.d.ts.map +1 -0
- package/lib/Bundle/PurePlugin.js +469 -0
- package/lib/Bundle/PurePlugin.js.map +1 -0
- package/lib/Bundle/index.d.ts +1 -0
- package/lib/Bundle/index.d.ts.map +1 -1
- package/lib/Bundle/index.js +1 -0
- package/lib/Bundle/index.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +19 -4
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +1 -1
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +10 -4
- package/lib/Plan.js.map +1 -1
- package/lib/Resource.d.ts +2 -2
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Telemetry/Attributes.d.ts +43 -0
- package/lib/Telemetry/Attributes.d.ts.map +1 -0
- package/lib/Telemetry/Attributes.js +144 -0
- package/lib/Telemetry/Attributes.js.map +1 -0
- package/lib/Telemetry/Layer.d.ts +14 -0
- package/lib/Telemetry/Layer.d.ts.map +1 -0
- package/lib/Telemetry/Layer.js +50 -0
- package/lib/Telemetry/Layer.js.map +1 -0
- package/lib/Telemetry/Metrics.d.ts +34 -0
- package/lib/Telemetry/Metrics.d.ts.map +1 -0
- package/lib/Telemetry/Metrics.js +57 -0
- package/lib/Telemetry/Metrics.js.map +1 -0
- package/package.json +12 -1
- package/src/Apply.ts +118 -9
- package/src/Axiom/Annotation.ts +95 -0
- package/src/Axiom/ApiToken.ts +125 -0
- package/src/Axiom/AuthProvider.ts +341 -0
- package/src/Axiom/Chart.ts +94 -0
- package/src/Axiom/Credentials.ts +70 -0
- package/src/Axiom/Dashboard.ts +192 -0
- package/src/Axiom/Dataset.ts +291 -0
- package/src/Axiom/Monitor.ts +122 -0
- package/src/Axiom/Notifier.ts +104 -0
- package/src/Axiom/Providers.ts +58 -0
- package/src/Axiom/View.ts +106 -0
- package/src/Axiom/VirtualField.ts +90 -0
- package/src/Axiom/index.ts +12 -0
- package/src/Bundle/Bundle.ts +39 -1
- package/src/Bundle/PurePlugin.ts +556 -0
- package/src/Bundle/index.ts +1 -0
- package/src/Cloudflare/Workers/Worker.ts +37 -6
- package/src/Output.ts +2 -1
- package/src/Plan.ts +13 -4
- package/src/Resource.ts +2 -2
- package/src/Telemetry/Attributes.ts +216 -0
- package/src/Telemetry/Layer.ts +65 -0
- 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
|
+
);
|