1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as observability from "@distilled.cloud/gcp/observability_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const waitUntilGone = (name: string) =>
18 observability.getProjectsLocationsBucketsDatasetsLinks({ name }).pipe(
19 Effect.as("found" as const),
20 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
21 Effect.repeat({
22 schedule: Schedule.spaced("2 seconds"),
23 until: (status) => status === "gone",
24 times: 10,
25 }),
26 );
27
28// The `_Trace` bucket and its `Spans` dataset only exist once the project
29// stores trace data in Observability; until then create fails with
30// BadRequest ("Unable to create link; parent bucket of … is not found").
31// Set GCP_TEST_OBSERVABILITY_DATASET to an existing dataset name.
32const spansDataset = process.env.GCP_TEST_OBSERVABILITY_DATASET;
33
34test.provider(
35 "getProjectsLocationsBucketsDatasetsLinks on a missing link fails with a typed tag",
36 (stack) =>
37 Effect.gen(function* () {
38 const { project } = yield* GcpEnvironment.current;
39 const defaultDataset = `projects/${project}/locations/us-central1/buckets/_Trace/datasets/Spans`;
40 yield* stack.destroy();
41
42 const error = yield* Effect.flip(
43 observability.getProjectsLocationsBucketsDatasetsLinks({
44 name: `${defaultDataset}/links/alchemy-missing-link`,
45 }),
46 );
47 expect(error._tag).toEqual("NotFound");
48
49 yield* stack.destroy();
50 }).pipe(logLevel),
51 {
52 tags: ["provider:gcp", "provider:gcp:observability", "live"],
53 timeout: 90_000,
54 },
55);
56
57test.provider.skipIf(!!spansDataset)(
58 "createProjectsLocationsBucketsDatasetsLinks without trace storage fails with BadRequest",
59 (stack) =>
60 Effect.gen(function* () {
61 const { project } = yield* GcpEnvironment.current;
62 yield* stack.destroy();
63
64 const error = yield* Effect.flip(
65 observability.createProjectsLocationsBucketsDatasetsLinks({
66 parent: `projects/${project}/locations/us-central1/buckets/_Trace/datasets/Spans`,
67 linkId: "alchemy_probe_link",
68 body: {},
69 }),
70 );
71 expect(error._tag).toEqual("BadRequest");
72 expect(error.message).toContain("parent bucket");
73
74 yield* stack.destroy();
75 }).pipe(logLevel),
76 {
77 tags: ["provider:gcp", "provider:gcp:observability", "live"],
78 timeout: 90_000,
79 },
80);
81
82test.provider.skipIf(!spansDataset)(
83 "create, update, and delete an observability dataset link",
84 (stack) =>
85 Effect.gen(function* () {
86 const { project } = yield* GcpEnvironment.current;
87 yield* stack.destroy();
88
89 const dataset = spansDataset!;
90
91 const created = yield* stack.deploy(
92 Effect.gen(function* () {
93 return yield* GCP.Observability.BucketsDatasetsLink("Analytics", {
94 dataset,
95 description: "bigquery analytics",
96 displayName: "Trace analytics",
97 });
98 }),
99 );
100
101 expect(created.linkId).toEqual(expect.any(String));
102 expect(created.linkId).toMatch(/^[a-z][a-z0-9_]*$/);
103 expect(created.dataset).toEqual(dataset);
104 expect(created.project).toEqual(project);
105 expect(created.name).toEqual(`${dataset}/links/${created.linkId}`);
106 expect(created.description).toEqual("bigquery analytics");
107 expect(created.displayName).toEqual("Trace analytics");
108
109 const fetched =
110 yield* observability.getProjectsLocationsBucketsDatasetsLinks({
111 name: created.name,
112 });
113 expect(fetched.name).toEqual(created.name);
114 expect(fetched.description).toContain("alchemy-id=");
115 expect(fetched.description).toContain("bigquery analytics");
116
117 const updated = yield* stack.deploy(
118 Effect.gen(function* () {
119 return yield* GCP.Observability.BucketsDatasetsLink("Analytics", {
120 dataset,
121 linkId: created.linkId,
122 description: "updated analytics",
123 displayName: "Updated traces",
124 });
125 }),
126 );
127
128 expect(updated.name).toEqual(created.name);
129 expect(updated.description).toEqual("updated analytics");
130 expect(updated.displayName).toEqual("Updated traces");
131
132 const fetchedUpdate =
133 yield* observability.getProjectsLocationsBucketsDatasetsLinks({
134 name: created.name,
135 });
136 expect(fetchedUpdate.description).toContain("updated analytics");
137 expect(fetchedUpdate.displayName).toEqual("Updated traces");
138
139 yield* stack.destroy();
140
141 const gone = yield* waitUntilGone(created.name);
142 expect(gone).toEqual("gone");
143 }).pipe(logLevel),
144 {
145 tags: ["provider:gcp", "provider:gcp:observability", "live"],
146 timeout: 120_000,
147 },
148);