@alchemy.run/axiom 0.0.0 → 2.0.0-beta.84

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 (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +54 -4
  3. package/lib/Annotation.d.ts +51 -0
  4. package/lib/Annotation.d.ts.map +1 -0
  5. package/lib/Annotation.js +97 -0
  6. package/lib/Annotation.js.map +1 -0
  7. package/lib/ApiToken.d.ts +64 -0
  8. package/lib/ApiToken.d.ts.map +1 -0
  9. package/lib/ApiToken.js +123 -0
  10. package/lib/ApiToken.js.map +1 -0
  11. package/lib/AuthProvider.d.ts +40 -0
  12. package/lib/AuthProvider.d.ts.map +1 -0
  13. package/lib/AuthProvider.js +99 -0
  14. package/lib/AuthProvider.js.map +1 -0
  15. package/lib/Chart.d.ts +130 -0
  16. package/lib/Chart.d.ts.map +1 -0
  17. package/lib/Chart.js +14 -0
  18. package/lib/Chart.js.map +1 -0
  19. package/lib/Contributions.d.ts +7 -0
  20. package/lib/Contributions.d.ts.map +1 -0
  21. package/lib/Contributions.js +6 -0
  22. package/lib/Contributions.js.map +1 -0
  23. package/lib/Credentials.d.ts +10 -0
  24. package/lib/Credentials.d.ts.map +1 -0
  25. package/lib/Credentials.js +30 -0
  26. package/lib/Credentials.js.map +1 -0
  27. package/lib/Dashboard.d.ts +112 -0
  28. package/lib/Dashboard.d.ts.map +1 -0
  29. package/lib/Dashboard.js +157 -0
  30. package/lib/Dashboard.js.map +1 -0
  31. package/lib/Dataset.d.ts +120 -0
  32. package/lib/Dataset.d.ts.map +1 -0
  33. package/lib/Dataset.js +215 -0
  34. package/lib/Dataset.js.map +1 -0
  35. package/lib/Monitor.d.ts +76 -0
  36. package/lib/Monitor.d.ts.map +1 -0
  37. package/lib/Monitor.js +126 -0
  38. package/lib/Monitor.js.map +1 -0
  39. package/lib/Notifier.d.ts +65 -0
  40. package/lib/Notifier.d.ts.map +1 -0
  41. package/lib/Notifier.js +112 -0
  42. package/lib/Notifier.js.map +1 -0
  43. package/lib/OtelHeaders.d.ts +9 -0
  44. package/lib/OtelHeaders.d.ts.map +1 -0
  45. package/lib/OtelHeaders.js +9 -0
  46. package/lib/OtelHeaders.js.map +1 -0
  47. package/lib/Providers.d.ts +15 -0
  48. package/lib/Providers.d.ts.map +1 -0
  49. package/lib/Providers.js +34 -0
  50. package/lib/Providers.js.map +1 -0
  51. package/lib/Telemetry.d.ts +66 -0
  52. package/lib/Telemetry.d.ts.map +1 -0
  53. package/lib/Telemetry.js +68 -0
  54. package/lib/Telemetry.js.map +1 -0
  55. package/lib/View.d.ts +57 -0
  56. package/lib/View.d.ts.map +1 -0
  57. package/lib/View.js +100 -0
  58. package/lib/View.js.map +1 -0
  59. package/lib/VirtualField.d.ts +44 -0
  60. package/lib/VirtualField.d.ts.map +1 -0
  61. package/lib/VirtualField.js +97 -0
  62. package/lib/VirtualField.js.map +1 -0
  63. package/lib/index.d.ts +14 -0
  64. package/lib/index.d.ts.map +1 -0
  65. package/lib/index.js +14 -0
  66. package/lib/index.js.map +1 -0
  67. package/package.json +53 -3
  68. package/src/Annotation.ts +121 -0
  69. package/src/ApiToken.ts +148 -0
  70. package/src/AuthProvider.ts +135 -0
  71. package/src/Chart.ts +156 -0
  72. package/src/Contributions.ts +9 -0
  73. package/src/Credentials.ts +68 -0
  74. package/src/Dashboard.ts +192 -0
  75. package/src/Dataset.ts +326 -0
  76. package/src/Monitor.ts +148 -0
  77. package/src/Notifier.ts +136 -0
  78. package/src/OtelHeaders.ts +9 -0
  79. package/src/Providers.ts +60 -0
  80. package/src/Telemetry.ts +116 -0
  81. package/src/View.ts +130 -0
  82. package/src/VirtualField.ts +123 -0
  83. package/src/index.ts +13 -0
@@ -0,0 +1,116 @@
1
+ import type { Input } from "alchemy/Input";
2
+ import * as Output from "alchemy/Output";
3
+ import { layerOtlp, type OtlpSignalOptions } from "alchemy/Telemetry";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Redacted from "effect/Redacted";
7
+ import type { ApiToken } from "./ApiToken.ts";
8
+ import type { Dataset } from "./Dataset.ts";
9
+ import { otelDatasetHeader } from "./OtelHeaders.ts";
10
+
11
+ /**
12
+ * A resource passed to the layer: either the module-scope declaration (an
13
+ * Effect that resolves to the instance, same as what capability bindings
14
+ * accept) or an already-yielded instance.
15
+ */
16
+ export type ResourceInput<T> = T | Effect.Effect<T, never, any>;
17
+
18
+ /**
19
+ * Options for {@link Telemetry}: an ingest {@link ApiToken} plus one
20
+ * {@link Dataset} per OTel signal to export.
21
+ */
22
+ export interface AxiomTelemetryProps {
23
+ /**
24
+ * The ingest credential. Must have `ingest: ["create"]` capability on
25
+ * every dataset passed below.
26
+ */
27
+ token: ResourceInput<ApiToken>;
28
+ /** Dataset (kind `otel:traces:v1`) to export traces into. */
29
+ traces?: ResourceInput<Dataset> | undefined;
30
+ /** Dataset (kind `otel:logs:v1`) to export logs into. */
31
+ logs?: ResourceInput<Dataset> | undefined;
32
+ /** Dataset (kind `otel:metrics:v1`) to export metrics into. */
33
+ metrics?: ResourceInput<Dataset> | undefined;
34
+ /**
35
+ * The exported `service.name`.
36
+ * @default the deployed Function/Worker's physical name
37
+ */
38
+ serviceName?: Input<string> | undefined;
39
+ }
40
+
41
+ /**
42
+ * Resource declarations are Effects — yield them to get the instance with
43
+ * attribute Output accessors, same as `Binding.Service`'s callable does for
44
+ * capability bindings.
45
+ */
46
+ const instance = <T>(resource: ResourceInput<T>): Effect.Effect<T> =>
47
+ Effect.isEffect(resource) ? (resource as Effect.Effect<T>) : Effect.succeed(resource);
48
+
49
+ const signal = (
50
+ token: ApiToken,
51
+ dataset: Dataset | undefined,
52
+ urlAttr: "otelTracesEndpoint" | "otelLogsEndpoint" | "otelMetricsEndpoint",
53
+ ): OtlpSignalOptions | undefined =>
54
+ dataset === undefined
55
+ ? undefined
56
+ : {
57
+ url: dataset[urlAttr],
58
+ headers: {
59
+ Authorization: Output.map(
60
+ token.token,
61
+ (bearer) =>
62
+ Redacted.make(`Bearer ${Redacted.value(bearer)}`) as Redacted.Redacted<string>,
63
+ ),
64
+ [otelDatasetHeader(urlAttr === "otelMetricsEndpoint")]: dataset.name,
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Export a Function/Worker's telemetry to Axiom.
70
+ *
71
+ * A binding layer over {@link layerOtlp | Alchemy.Telemetry.layerOtlp}:
72
+ * building it binds each dataset's OTLP endpoint and the ingest token's
73
+ * `Authorization` header (as a secret) onto the host, and at runtime the
74
+ * built-in exporter ships each signal to its dataset, flushed per event.
75
+ *
76
+ * Compose it into the Function/Worker's single `Effect.provide`:
77
+ *
78
+ * ```ts
79
+ * import * as Axiom from "@alchemy.run/axiom";
80
+ * import { Bucket } from "./bucket.ts";
81
+ * import { Ingest, Logs, Traces } from "./observability.ts";
82
+ *
83
+ * export default Cloudflare.Worker(
84
+ * "Worker",
85
+ * { main: import.meta.url },
86
+ * Effect.gen(function* () {
87
+ * // ...
88
+ * }).pipe(
89
+ * Effect.provide(
90
+ * Layer.mergeAll(
91
+ * Cloudflare.R2.ReadWriteBucketBinding,
92
+ * Axiom.Telemetry({ token: Ingest, traces: Traces, logs: Logs }),
93
+ * ),
94
+ * ),
95
+ * ),
96
+ * );
97
+ * ```
98
+ */
99
+ export const Telemetry = (props: AxiomTelemetryProps): Layer.Layer<never> =>
100
+ Layer.unwrap(
101
+ Effect.gen(function* () {
102
+ // Declarations are yielded to instances (registering them on the
103
+ // Stack if the enclosing host is the first to reference them), so
104
+ // attribute accessors below produce real Outputs.
105
+ const token = yield* instance(props.token);
106
+ const traces = props.traces && (yield* instance(props.traces));
107
+ const logs = props.logs && (yield* instance(props.logs));
108
+ const metrics = props.metrics && (yield* instance(props.metrics));
109
+ return layerOtlp({
110
+ serviceName: props.serviceName,
111
+ traces: signal(token, traces, "otelTracesEndpoint"),
112
+ logs: signal(token, logs, "otelLogsEndpoint"),
113
+ metrics: signal(token, metrics, "otelMetricsEndpoint"),
114
+ });
115
+ }),
116
+ ) as Layer.Layer<never>;
package/src/View.ts ADDED
@@ -0,0 +1,130 @@
1
+ import * as Axiom from "@distilled.cloud/axiom";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import * as Effect from "effect/Effect";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export type ViewProps = Axiom.CreateViewRequest;
9
+
10
+ export type View = Resource<
11
+ "Axiom.View",
12
+ ViewProps,
13
+ Axiom.View & {
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
+ * @see https://axiom.co/docs/query-data/datasets — APL query reference
33
+ *
34
+ * ### Creating a View
35
+ * **Example:** Recent errors across one dataset
36
+ * ```typescript
37
+ * yield* Axiom.View("recent-errors", {
38
+ * name: "recent-errors",
39
+ * description: "Last 100 5xx responses",
40
+ * datasets: ["my-app-traces"],
41
+ * aplQuery: `
42
+ * ['my-app-traces']
43
+ * | where status >= 500
44
+ * | order by _time desc
45
+ * | take 100
46
+ * `,
47
+ * });
48
+ * ```
49
+ *
50
+ * **Example:** Cross-dataset join (logs + traces by trace_id)
51
+ * ```typescript
52
+ * yield* Axiom.View("trace-with-logs", {
53
+ * name: "trace-with-logs",
54
+ * datasets: ["my-app-traces", "my-app-logs"],
55
+ * aplQuery: `
56
+ * ['my-app-traces']
57
+ * | where duration_ms > 1000
58
+ * | join kind=leftouter (['my-app-logs']) on trace_id
59
+ * `,
60
+ * });
61
+ * ```
62
+ *
63
+ * @resource
64
+ * @product Dashboard
65
+ */
66
+ export const View = Resource<View>("Axiom.View");
67
+
68
+ export const ViewProvider = () =>
69
+ Provider.effect(
70
+ View,
71
+ Effect.gen(function* () {
72
+ const create = yield* Axiom.createView;
73
+ const update = yield* Axiom.updateView;
74
+ const get = yield* Axiom.getView;
75
+ const listViews = yield* Axiom.getViews;
76
+ const del = yield* Axiom.deleteView;
77
+
78
+ return {
79
+ stables: ["id"],
80
+ // Enumerate every view in the org. Axiom exposes a single account-wide
81
+ // `GET /v2/views` collection op (no pagination) that already returns
82
+ // the full view objects, so we fetch it once and hydrate each row into
83
+ // the exact `read` Attributes shape (`id` is derived from `name`, the
84
+ // path identifier) — directly usable by `delete` with no follow-up get.
85
+ list: () =>
86
+ Effect.gen(function* () {
87
+ const views = yield* listViews({});
88
+ return views.map((view) => ({ ...view, id: view.name }));
89
+ }),
90
+ diff: Effect.fn(function* ({ news, output }) {
91
+ if (!isResolved(news)) return undefined;
92
+ if (output && news.name !== output.name) {
93
+ return { action: "replace" } as const;
94
+ }
95
+ return undefined;
96
+ }),
97
+ reconcile: Effect.fn(function* ({ news, output }) {
98
+ // Observe — `name` is the path identifier for views. Renames are
99
+ // forced to a replacement by `diff` above, so the cached
100
+ // `output.id` (set to `news.name` on first create) and the
101
+ // current `news.name` always agree by the time we land here.
102
+ const viewId = output?.id ?? news.name;
103
+ const observed = yield* get({ id: viewId }).pipe(
104
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
105
+ );
106
+
107
+ // Ensure — POST creates the view under `news.name`.
108
+ if (observed === undefined) {
109
+ const result = yield* create(news);
110
+ return { ...result, id: news.name };
111
+ }
112
+
113
+ // Sync — the view exists; PUT against its id with the desired
114
+ // props.
115
+ const result = yield* update({ ...news, id: viewId });
116
+ return { ...result, id: viewId };
117
+ }),
118
+ delete: Effect.fn(function* ({ output }) {
119
+ yield* del({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
120
+ }),
121
+ read: Effect.fn(function* ({ output }) {
122
+ if (!output?.id) return undefined;
123
+ return yield* get({ id: output.id }).pipe(
124
+ Effect.map((current) => ({ ...current, id: output.id })),
125
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
126
+ );
127
+ }),
128
+ };
129
+ }),
130
+ );
@@ -0,0 +1,123 @@
1
+ import * as Axiom from "@distilled.cloud/axiom";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import * as Effect from "effect/Effect";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export type VirtualFieldProps = Axiom.CreateVirtualFieldRequest;
9
+
10
+ export type VirtualField = Resource<
11
+ "Axiom.VirtualField",
12
+ VirtualFieldProps,
13
+ Axiom.CreateVirtualFieldResponse,
14
+ never,
15
+ Providers
16
+ >;
17
+
18
+ /**
19
+ * An Axiom virtual field — a saved APL expression that appears as a derived
20
+ * column on a dataset at query time. Use these to standardise common
21
+ * computations (status classes, latency buckets, parsed JSON paths) so
22
+ * dashboards and monitors don't have to redefine them.
23
+ *
24
+ * Bound to a single `dataset`; changing the dataset triggers a replacement.
25
+ * @see https://axiom.co/docs/query-data/virtual-fields
26
+ *
27
+ * ### Creating a Virtual Field
28
+ * **Example:** HTTP status class (e.g. 200 → "2xx")
29
+ * ```typescript
30
+ * yield* Axiom.VirtualField("status-class", {
31
+ * dataset: "my-app-traces",
32
+ * name: "status_class",
33
+ * description: "HTTP response class bucket",
34
+ * expression: 'strcat(tostring(toint(status / 100)), "xx")',
35
+ * type: "string",
36
+ * });
37
+ * ```
38
+ *
39
+ * **Example:** Latency bucket in seconds
40
+ * ```typescript
41
+ * yield* Axiom.VirtualField("latency-bucket", {
42
+ * dataset: "my-app-traces",
43
+ * name: "latency_bucket_s",
44
+ * expression: "bin(duration_ms / 1000.0, 0.5)",
45
+ * type: "number",
46
+ * unit: "s",
47
+ * });
48
+ * ```
49
+ *
50
+ * @resource
51
+ * @product Dataset
52
+ */
53
+ export const VirtualField = Resource<VirtualField>("Axiom.VirtualField");
54
+
55
+ export const VirtualFieldProvider = () =>
56
+ Provider.effect(
57
+ VirtualField,
58
+ Effect.gen(function* () {
59
+ const create = yield* Axiom.createVirtualField;
60
+ const update = yield* Axiom.updateVirtualField;
61
+ const get = yield* Axiom.getVirtualField;
62
+ const del = yield* Axiom.deleteVirtualField;
63
+ const listVirtualFields = yield* Axiom.getVirtualFields;
64
+ const listDatasets = yield* Axiom.getDatasets;
65
+
66
+ return {
67
+ stables: ["id"],
68
+ // Axiom only exposes a per-dataset virtual-field enumeration
69
+ // (`GET /v2/vfields?dataset=...`); there is no account-wide list. So
70
+ // enumerate every dataset (`GET /v2/datasets`), fan out the per-dataset
71
+ // vfields list with bounded concurrency, and flatten. Each row already
72
+ // matches the `read` Attributes shape (`CreateVirtualFieldOutput`), so
73
+ // it's directly usable by `delete` with no follow-up read.
74
+ list: () =>
75
+ Effect.gen(function* () {
76
+ const datasets = yield* listDatasets({});
77
+ const perDataset = yield* Effect.forEach(
78
+ datasets,
79
+ (ds) => listVirtualFields({ dataset: ds.name }),
80
+ { concurrency: 10 },
81
+ );
82
+ return perDataset.flat().map((vf) => ({ ...vf }));
83
+ }),
84
+ diff: Effect.fn(function* ({ news, output }) {
85
+ if (!isResolved(news)) return undefined;
86
+ if (output && news.dataset !== output.dataset) {
87
+ return { action: "replace" } as const;
88
+ }
89
+ return undefined;
90
+ }),
91
+ reconcile: Effect.fn(function* ({ news, output }) {
92
+ // Observe — Axiom assigns the virtual-field id server-side, so
93
+ // the only handle to a previously-created field is the cached
94
+ // `output.id`. Probe for live state with that id; treat NotFound
95
+ // (deleted out-of-band) as "no observed state" so we converge by
96
+ // re-creating.
97
+ const observed = output?.id
98
+ ? yield* get({ id: output.id }).pipe(
99
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
100
+ )
101
+ : undefined;
102
+
103
+ // Ensure — POST mints a new virtual field with a fresh id.
104
+ if (observed === undefined) {
105
+ return yield* create(news);
106
+ }
107
+
108
+ // Sync — the field exists; PUT against its id with the desired
109
+ // props. `dataset` is replacement-only (handled in diff).
110
+ return yield* update({ ...news, id: observed.id });
111
+ }),
112
+ delete: Effect.fn(function* ({ output }) {
113
+ yield* del({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
114
+ }),
115
+ read: Effect.fn(function* ({ output }) {
116
+ if (!output?.id) return undefined;
117
+ return yield* get({ id: output.id }).pipe(
118
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
119
+ );
120
+ }),
121
+ };
122
+ }),
123
+ );
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./Annotation.ts";
2
+ export * from "./ApiToken.ts";
3
+ export * from "./AuthProvider.ts";
4
+ export * from "./Chart.ts";
5
+ export * from "./Credentials.ts";
6
+ export * from "./Dashboard.ts";
7
+ export * from "./Dataset.ts";
8
+ export * from "./Monitor.ts";
9
+ export * from "./Notifier.ts";
10
+ export * from "./Telemetry.ts";
11
+ export * from "./Providers.ts";
12
+ export * from "./View.ts";
13
+ export * from "./VirtualField.ts";