1import * as apigee from "@distilled.cloud/gcp/apigee_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 environmentIdOf,
12 environmentNameOf,
13 lastSegment,
14 listProjectEnvironments,
15 missingToUndefined,
16 organizationIdOf,
17 parseOrgEnv,
18 sameText,
19} from "./common.ts";
20
21export type TraceSamplingConfig = {
22 /**
23 * Sampler. `OFF` is the default; `PROBABILITY` uses `samplingRate`.
24 */
25 sampler?:
26 | apigee.GoogleCloudApigeeV1TraceSamplingConfigSamplerEnum
27 | (string & {});
28 /**
29 * Sampling rate when using `PROBABILITY`. Must be `> 0` and `<= 0.5`.
30 */
31 samplingRate?: number;
32};
33
34export type EnvironmentsTraceConfigOverrideProps = {
35 /**
36 * Apigee organization id or `organizations/{org}`. Defaults to the
37 * current GCP project id. Immutable — changing it replaces the override.
38 */
39 organization?: string;
40 /**
41 * Environment id or `organizations/{org}/environments/{env}`. Immutable —
42 * changing it replaces the override.
43 */
44 environment: string;
45 /**
46 * API proxy whose trace configuration is overridden.
47 */
48 apiProxy: string;
49 /**
50 * Trace sampling configuration to apply.
51 */
52 samplingConfig?: TraceSamplingConfig;
53};
54
55export type EnvironmentsTraceConfigOverride = Resource<
56 "GCP.Apigee.EnvironmentsTraceConfigOverride",
57 EnvironmentsTraceConfigOverrideProps,
58 {
59 /** Full resource name `organizations/{org}/environments/{env}/traceConfig/overrides/{id}`. */
60 name: string;
61 /** System-generated override id (UUID). */
62 traceConfigOverrideId: string;
63 /** Apigee organization id. */
64 organizationId: string;
65 /** Environment id. */
66 environmentId: string;
67 /** API proxy id. */
68 apiProxy: string;
69 /** Sampling configuration. */
70 samplingConfig: TraceSamplingConfig | undefined;
71 },
72 never,
73 Providers
74>;
75
76/**
77 * A distributed-trace configuration override for one API proxy in an
78 * Apigee environment.
79 *
80 * Overrides have no labels; `list` enumerates every override in Apigee
81 * environments mapped to this GCP project. The override id is assigned by
82 * Apigee. Organization and environment are identity; `apiProxy` and
83 * `samplingConfig` update in place.
84 *
85 * ### Creating an Override
86 * **Example:** Probability sampler
87 * ```typescript
88 * const override = yield* GCP.Apigee.EnvironmentsTraceConfigOverride("ProxyTrace", {
89 * environment: "eval",
90 * apiProxy: "hello",
91 * samplingConfig: { sampler: "PROBABILITY", samplingRate: 0.1 },
92 * });
93 * ```
94 *
95 * @resource
96 * @category Apigee
97 */
98export const EnvironmentsTraceConfigOverride =
99 Resource<EnvironmentsTraceConfigOverride>(
100 "GCP.Apigee.EnvironmentsTraceConfigOverride",
101 );
102
103export class EnvironmentsTraceConfigOverrideNotResolved extends Data.TaggedError(
104 "GCP.Apigee.EnvironmentsTraceConfigOverrideNotResolved",
105)<{
106 name: string;
107}> {}
108
109const traceConfigParent = (organizationId: string, environmentId: string) =>
110 `${environmentNameOf(organizationId, environmentId)}/traceConfig`;
111
112const samplingOf = (
113 config: apigee.GoogleCloudApigeeV1TraceSamplingConfig | undefined,
114): TraceSamplingConfig | undefined => {
115 if (config === undefined) return undefined;
116 return { sampler: config.sampler, samplingRate: config.samplingRate };
117};
118
119const toAttrs = (
120 override: apigee.GoogleCloudApigeeV1TraceConfigOverride,
121 organizationId: string,
122 environmentId: string,
123) => {
124 const raw = override.name ?? "";
125 const parsed = parseOrgEnv(raw);
126 return {
127 name: raw.includes("/")
128 ? raw
129 : `${traceConfigParent(organizationId, environmentId)}/overrides/${raw}`,
130 traceConfigOverrideId: lastSegment(raw),
131 organizationId: parsed.organizationId || organizationId,
132 environmentId: parsed.environmentId || environmentId,
133 apiProxy: override.apiProxy ?? "",
134 samplingConfig: samplingOf(override.samplingConfig),
135 };
136};
137
138const getByName = (name: string) =>
139 missingToUndefined(
140 apigee.getOrganizationsEnvironmentsTraceConfigOverrides({ name }),
141 );
142
143const listOverrides = (parent: string) =>
144 apigee.listOrganizationsEnvironmentsTraceConfigOverrides
145 .pages({ parent, pageSize: 100 })
146 .pipe(
147 Stream.flatMap((page) =>
148 Stream.fromIterable(page.traceConfigOverrides ?? []),
149 ),
150 Stream.runCollect,
151 Effect.map((chunk) => Array.from(chunk)),
152 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
153 Effect.succeed([] as apigee.GoogleCloudApigeeV1TraceConfigOverride[]),
154 ),
155 );
156
157const jsonOf = (value: unknown) => JSON.stringify(value ?? null);
158
159export const EnvironmentsTraceConfigOverrideProvider = () =>
160 Provider.succeed(EnvironmentsTraceConfigOverride, {
161 stables: [
162 "name",
163 "traceConfigOverrideId",
164 "organizationId",
165 "environmentId",
166 ],
167
168 diff: Effect.fn(function* ({ news, olds, output }) {
169 if (!isResolved(news)) return undefined;
170 const previousOrg = olds?.organization ?? output?.organizationId;
171 const previousEnv = olds?.environment ?? output?.environmentId;
172 const orgChanged =
173 previousOrg !== undefined &&
174 news.organization !== undefined &&
175 organizationIdOf(news.organization, "") !==
176 organizationIdOf(previousOrg, "");
177 const envChanged =
178 previousEnv !== undefined &&
179 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
180 if (orgChanged || envChanged) {
181 return { action: "replace" as const, deleteFirst: false };
182 }
183 return undefined;
184 }),
185
186 read: Effect.fn(function* ({ olds, output }) {
187 const { project } = yield* GcpEnvironment.current;
188 const organizationId = organizationIdOf(
189 olds?.organization ?? output?.organizationId,
190 project,
191 );
192 const environmentId = environmentIdOf(
193 olds?.environment ?? output?.environmentId ?? "",
194 );
195 if (output?.name) {
196 const existing = yield* getByName(output.name);
197 if (existing === undefined) return undefined;
198 return toAttrs(existing, organizationId, environmentId);
199 }
200 const listed = yield* listOverrides(
201 traceConfigParent(organizationId, environmentId),
202 );
203 const match = listed.find(
204 (item) => item.apiProxy === (olds?.apiProxy ?? ""),
205 );
206 if (match === undefined) return undefined;
207 return toAttrs(match, organizationId, environmentId);
208 }),
209
210 list: () =>
211 Effect.gen(function* () {
212 const environments = yield* listProjectEnvironments();
213 const found: EnvironmentsTraceConfigOverride["Attributes"][] = [];
214 for (const item of environments) {
215 const listed = yield* listOverrides(
216 traceConfigParent(item.organizationId, item.environmentId),
217 );
218 for (const override of listed) {
219 found.push(
220 toAttrs(override, item.organizationId, item.environmentId),
221 );
222 }
223 }
224 return found;
225 }),
226
227 reconcile: Effect.fn(function* ({ news, output }) {
228 const { project } = yield* GcpEnvironment.current;
229 const organizationId = organizationIdOf(news.organization, project);
230 const environmentId = environmentIdOf(news.environment);
231 const parent = traceConfigParent(organizationId, environmentId);
232
233 let current =
234 output?.name !== undefined ? yield* getByName(output.name) : undefined;
235 if (current === undefined) {
236 const listed = yield* listOverrides(parent);
237 current = listed.find((item) => item.apiProxy === news.apiProxy);
238 }
239
240 if (current === undefined) {
241 const created = yield* apigee
242 .createOrganizationsEnvironmentsTraceConfigOverrides({
243 parent,
244 body: {
245 apiProxy: news.apiProxy,
246 samplingConfig: news.samplingConfig,
247 },
248 })
249 .pipe(
250 Effect.catchTag("Conflict", () =>
251 listOverrides(parent).pipe(
252 Effect.map((items) =>
253 items.find((item) => item.apiProxy === news.apiProxy),
254 ),
255 ),
256 ),
257 );
258 current = created ?? undefined;
259 }
260
261 if (current === undefined || !current.name) {
262 return yield* new EnvironmentsTraceConfigOverrideNotResolved({
263 name: `${parent}/overrides`,
264 });
265 }
266
267 const proxyChanged = !sameText(current.apiProxy, news.apiProxy);
268 const samplingChanged =
269 jsonOf(samplingOf(current.samplingConfig)) !==
270 jsonOf(news.samplingConfig);
271 if (proxyChanged || samplingChanged) {
272 current =
273 yield* apigee.patchOrganizationsEnvironmentsTraceConfigOverrides({
274 name: current.name,
275 updateMask: [
276 proxyChanged ? "api_proxy" : undefined,
277 samplingChanged ? "sampling_config" : undefined,
278 ]
279 .filter((field): field is string => field !== undefined)
280 .join(","),
281 body: {
282 apiProxy: news.apiProxy,
283 samplingConfig: news.samplingConfig,
284 },
285 });
286 }
287
288 return toAttrs(current, organizationId, environmentId);
289 }),
290
291 delete: Effect.fn(function* ({ output }) {
292 yield* apigee
293 .deleteOrganizationsEnvironmentsTraceConfigOverrides({
294 name: output.name,
295 })
296 .pipe(
297 Effect.catchTag(
298 ["NotFound", "ApigeeResourceNotFound"],
299 () => Effect.void,
300 ),
301 );
302 }),
303 });