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
package/bin/alchemy.ts CHANGED
@@ -7,6 +7,7 @@ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
7
7
  import packageJson from "../package.json" with { type: "json" };
8
8
  import { AlchemyContextLive } from "../src/AlchemyContext.ts";
9
9
  import { inkCLI } from "../src/Cli/InkCLI.tsx";
10
+ import { TelemetryLive } from "../src/Telemetry/Layer.ts";
10
11
  import { PlatformServices, runMain } from "../src/Util/PlatformServices.ts";
11
12
 
12
13
  import { handleCancellation } from "./commands/_shared.ts";
@@ -45,6 +46,7 @@ const services = Layer.mergeAll(
45
46
  Layer.provideMerge(AlchemyContextLive, PlatformServices),
46
47
  FetchHttpClient.layer,
47
48
  ConfigProvider.layer(ConfigProvider.fromEnv()),
49
+ TelemetryLive,
48
50
  inkCLI(),
49
51
  );
50
52
 
@@ -11,6 +11,7 @@ import * as Flag from "effect/unstable/cli/Flag";
11
11
 
12
12
  import { AuthError } from "../../src/Auth/AuthProvider.ts";
13
13
  import type * as Stack from "../../src/Stack.ts";
14
+ import { recordCli } from "../../src/Telemetry/Metrics.ts";
14
15
  import { PromptCancelled } from "../../src/Util/Clank.ts";
15
16
 
16
17
  export const USER = Config.string("USER").pipe(
@@ -218,6 +219,39 @@ export const parseSince = (value: string): Date => {
218
219
  return parsed;
219
220
  };
220
221
 
222
+ /**
223
+ * Wraps a CLI command handler with a top-level OpenTelemetry span
224
+ * (`cli.<command>`) and bumps the `alchemy.cli.invocations` counter.
225
+ *
226
+ * `attrs` runs against the parsed command args and contributes
227
+ * additional attributes to the span (e.g. stage, profile, dry-run flag).
228
+ *
229
+ * Usage:
230
+ * ```ts
231
+ * Command.make(
232
+ * "deploy",
233
+ * { ...flags },
234
+ * instrumentCommand("deploy", (a) => ({
235
+ * "alchemy.stage": a.stage,
236
+ * "alchemy.profile": a.profile,
237
+ * }))(execStack),
238
+ * );
239
+ * ```
240
+ */
241
+ export const instrumentCommand =
242
+ <Args>(
243
+ command: string,
244
+ attrs: (args: Args) => Record<string, unknown> = () => ({}),
245
+ ) =>
246
+ <A, E, R>(
247
+ handler: (args: Args) => Effect.Effect<A, E, R>,
248
+ ): ((args: Args) => Effect.Effect<A, E, R>) =>
249
+ (args) =>
250
+ handler(args).pipe(
251
+ Effect.withSpan(`cli.${command}`, { attributes: attrs(args) }),
252
+ recordCli(command),
253
+ );
254
+
221
255
  export const importStack = Effect.fn(function* (main: string) {
222
256
  const path = yield* Path.Path;
223
257
  const module = yield* Effect.promise(
@@ -21,7 +21,7 @@ import * as AWSRegion from "../../src/AWS/Region.ts";
21
21
  import { loadConfigProvider } from "../../src/Util/ConfigProvider.ts";
22
22
  import { fileLogger } from "../../src/Util/FileLogger.ts";
23
23
 
24
- import { envFile } from "./_shared.ts";
24
+ import { envFile, instrumentCommand } from "./_shared.ts";
25
25
 
26
26
  const awsProfile = Flag.string("profile").pipe(
27
27
  Flag.withDescription("AWS profile to use for credentials"),
@@ -50,7 +50,14 @@ export const bootstrapCommand = Command.make(
50
50
  region: awsRegion,
51
51
  destroy: bootstrapDestroy,
52
52
  },
53
- Effect.fnUntraced(function* ({ envFile, profile, region, destroy }) {
53
+ instrumentCommand(
54
+ "bootstrap",
55
+ (a: { profile: string; region: string | undefined; destroy: boolean }) => ({
56
+ "alchemy.profile": a.profile,
57
+ "alchemy.region": a.region ?? "",
58
+ "alchemy.destroy": a.destroy,
59
+ }),
60
+ )(Effect.fnUntraced(function* ({ envFile, profile, region, destroy }) {
54
61
  const logger = Logger.layer([fileLogger("bootstrap.txt")]);
55
62
 
56
63
  return yield* Effect.gen(function* () {
@@ -113,5 +120,5 @@ export const bootstrapCommand = Command.make(
113
120
  );
114
121
  });
115
122
  }).pipe(Effect.provide(logger));
116
- }),
123
+ })),
117
124
  );
@@ -21,12 +21,30 @@ import {
21
21
  envFile,
22
22
  force,
23
23
  importStack,
24
+ instrumentCommand,
24
25
  main,
25
26
  profile,
26
27
  stage,
27
28
  yes,
28
29
  } from "./_shared.ts";
29
30
 
31
+ const stackSpanAttrs = (args: {
32
+ main: string;
33
+ stage: string;
34
+ profile: string;
35
+ dryRun?: boolean;
36
+ force?: boolean;
37
+ yes?: boolean;
38
+ destroy?: boolean;
39
+ }) => ({
40
+ "alchemy.stage": args.stage,
41
+ "alchemy.profile": args.profile,
42
+ "alchemy.main": args.main,
43
+ "alchemy.dry_run": !!args.dryRun,
44
+ "alchemy.force": !!args.force,
45
+ "alchemy.destroy": !!args.destroy,
46
+ });
47
+
30
48
  const execStack = Effect.fn(function* ({
31
49
  main,
32
50
  stage,
@@ -118,7 +136,7 @@ export const deployCommand = Command.make(
118
136
  yes,
119
137
  profile,
120
138
  },
121
- execStack,
139
+ instrumentCommand("deploy", stackSpanAttrs)(execStack),
122
140
  );
123
141
 
124
142
  export const destroyCommand = Command.make(
@@ -131,11 +149,12 @@ export const destroyCommand = Command.make(
131
149
  yes,
132
150
  profile,
133
151
  },
134
- (args) =>
152
+ instrumentCommand("destroy", stackSpanAttrs)((args) =>
135
153
  execStack({
136
154
  ...args,
137
155
  destroy: true,
138
156
  }),
157
+ ),
139
158
  );
140
159
 
141
160
  export const planCommand = Command.make(
@@ -146,10 +165,11 @@ export const planCommand = Command.make(
146
165
  stage,
147
166
  profile,
148
167
  },
149
- (args) =>
168
+ instrumentCommand("plan", stackSpanAttrs)((args) =>
150
169
  execStack({
151
170
  ...args,
152
171
  // plan is the same as deploy with dryRun always set to true
153
172
  dryRun: true,
154
173
  }),
174
+ ),
155
175
  );
@@ -18,7 +18,14 @@ import * as State from "../../src/State/index.ts";
18
18
  import { loadConfigProvider } from "../../src/Util/ConfigProvider.ts";
19
19
  import { fileLogger } from "../../src/Util/FileLogger.ts";
20
20
 
21
- import { envFile, importStack, main, profile, stage } from "./_shared.ts";
21
+ import {
22
+ envFile,
23
+ importStack,
24
+ instrumentCommand,
25
+ main,
26
+ profile,
27
+ stage,
28
+ } from "./_shared.ts";
22
29
 
23
30
  const loginConfigure = Flag.boolean("configure").pipe(
24
31
  Flag.withDescription(
@@ -36,7 +43,15 @@ export const loginCommand = Command.make(
36
43
  profile,
37
44
  configure: loginConfigure,
38
45
  },
39
- Effect.fnUntraced(function* ({ main, stage, envFile, profile, configure }) {
46
+ instrumentCommand(
47
+ "login",
48
+ (a: { main: string; stage: string; profile: string; configure: boolean }) => ({
49
+ "alchemy.stage": a.stage,
50
+ "alchemy.profile": a.profile,
51
+ "alchemy.main": a.main,
52
+ "alchemy.configure": a.configure,
53
+ }),
54
+ )(Effect.fnUntraced(function* ({ main, stage, envFile, profile, configure }) {
40
55
  const stackEffect = yield* importStack(main);
41
56
 
42
57
  const authProviders: AuthProviders["Service"] = {};
@@ -92,5 +107,5 @@ export const loginCommand = Command.make(
92
107
  { discard: true },
93
108
  );
94
109
  }).pipe(Effect.provide(services));
95
- }),
110
+ })),
96
111
  );
@@ -18,6 +18,7 @@ import {
18
18
  envFile,
19
19
  formatLocalTimestamp,
20
20
  importStack,
21
+ instrumentCommand,
21
22
  main,
22
23
  parseResourceFilter,
23
24
  parseSince,
@@ -52,7 +53,15 @@ export const logsCommand = Command.make(
52
53
  limit: logsLimit,
53
54
  since: logsSince,
54
55
  },
55
- Effect.fnUntraced(function* ({
56
+ instrumentCommand(
57
+ "logs",
58
+ (a: { main: string; stage: string; profile: string; limit: number }) => ({
59
+ "alchemy.stage": a.stage,
60
+ "alchemy.profile": a.profile,
61
+ "alchemy.main": a.main,
62
+ "alchemy.limit": a.limit,
63
+ }),
64
+ )(Effect.fnUntraced(function* ({
56
65
  main,
57
66
  stage,
58
67
  envFile,
@@ -153,5 +162,5 @@ export const logsCommand = Command.make(
153
162
  }
154
163
  }).pipe(Effect.provide(stack.services));
155
164
  }).pipe(Effect.provide(services));
156
- }),
165
+ })),
157
166
  );
@@ -18,12 +18,15 @@ import { CloudflareAuth } from "../../src/Cloudflare/Auth/AuthProvider.ts";
18
18
  import { loadConfigProvider } from "../../src/Util/ConfigProvider.ts";
19
19
  import { fileLogger } from "../../src/Util/FileLogger.ts";
20
20
 
21
- import { envFile, profile } from "./_shared.ts";
21
+ import { envFile, instrumentCommand, profile } from "./_shared.ts";
22
22
 
23
23
  const showCommand = Command.make(
24
24
  "show",
25
25
  { profile, envFile },
26
- Effect.fnUntraced(function* ({ profile, envFile }) {
26
+ instrumentCommand(
27
+ "profile.show",
28
+ (a: { profile: string }) => ({ "alchemy.profile": a.profile }),
29
+ )(Effect.fnUntraced(function* ({ profile, envFile }) {
27
30
  const stored = yield* getProfile(profile);
28
31
  if (stored == null) {
29
32
  const config = yield* readConfig;
@@ -80,13 +83,16 @@ const showCommand = Command.make(
80
83
  yield* provider.prettyPrint(profile, cfg);
81
84
  }
82
85
  }).pipe(Effect.provide(services));
83
- }),
86
+ })),
84
87
  );
85
88
 
86
89
  const clearCommand = Command.make(
87
90
  "clear",
88
91
  { profile },
89
- Effect.fnUntraced(function* ({ profile }) {
92
+ instrumentCommand(
93
+ "profile.clear",
94
+ (a: { profile: string }) => ({ "alchemy.profile": a.profile }),
95
+ )(Effect.fnUntraced(function* ({ profile }) {
90
96
  const removed = yield* deleteProfile(profile);
91
97
  yield* deleteProfileCredentials(profile);
92
98
  if (removed) {
@@ -98,7 +104,7 @@ const clearCommand = Command.make(
98
104
  `Profile '${profile}' not found in profiles.json; removed any stray credentials directory.`,
99
105
  );
100
106
  }
101
- }),
107
+ })),
102
108
  );
103
109
 
104
110
  export const profileCommand = Command.make("profile", {}).pipe(
@@ -15,7 +15,15 @@ import * as Clank from "../../src/Util/Clank.ts";
15
15
  import { loadConfigProvider } from "../../src/Util/ConfigProvider.ts";
16
16
  import { fileLogger } from "../../src/Util/FileLogger.ts";
17
17
 
18
- import { envFile, importStack, main, profile, stage, yes } from "./_shared.ts";
18
+ import {
19
+ envFile,
20
+ importStack,
21
+ instrumentCommand,
22
+ main,
23
+ profile,
24
+ stage,
25
+ yes,
26
+ } from "./_shared.ts";
19
27
 
20
28
  /**
21
29
  * When set, the State service is replaced with the on-disk
@@ -91,7 +99,7 @@ const withStateService = <A, E>(
91
99
  const stacksCommand = Command.make(
92
100
  "stacks",
93
101
  { main, envFile, stage, profile, local: localFlag },
94
- Effect.fnUntraced(function* (args) {
102
+ instrumentCommand("state.stacks")(Effect.fnUntraced(function* (args) {
95
103
  yield* withStateService(args, (state) =>
96
104
  Effect.gen(function* () {
97
105
  const stacks = yield* state.listStacks();
@@ -104,13 +112,13 @@ const stacksCommand = Command.make(
104
112
  }
105
113
  }),
106
114
  );
107
- }),
115
+ })),
108
116
  );
109
117
 
110
118
  const stagesCommand = Command.make(
111
119
  "stages",
112
120
  { stack: stackArg, main, envFile, stage, profile, local: localFlag },
113
- Effect.fnUntraced(function* ({ stack: stackName, ...rest }) {
121
+ instrumentCommand("state.stages")(Effect.fnUntraced(function* ({ stack: stackName, ...rest }) {
114
122
  yield* withStateService(rest, (state) =>
115
123
  Effect.gen(function* () {
116
124
  const stages = yield* state.listStages(stackName);
@@ -123,7 +131,7 @@ const stagesCommand = Command.make(
123
131
  }
124
132
  }),
125
133
  );
126
- }),
134
+ })),
127
135
  );
128
136
 
129
137
  const resourcesCommand = Command.make(
@@ -139,7 +147,7 @@ const resourcesCommand = Command.make(
139
147
  profile,
140
148
  local: localFlag,
141
149
  },
142
- Effect.fnUntraced(function* ({ stack: stackName, stageName, ...rest }) {
150
+ instrumentCommand("state.resources")(Effect.fnUntraced(function* ({ stack: stackName, stageName, ...rest }) {
143
151
  yield* withStateService(rest, (state) =>
144
152
  Effect.gen(function* () {
145
153
  const fqns = yield* state.list({ stack: stackName, stage: stageName });
@@ -152,7 +160,7 @@ const resourcesCommand = Command.make(
152
160
  }
153
161
  }),
154
162
  );
155
- }),
163
+ })),
156
164
  );
157
165
 
158
166
  const getCommand = Command.make(
@@ -169,7 +177,7 @@ const getCommand = Command.make(
169
177
  profile,
170
178
  local: localFlag,
171
179
  },
172
- Effect.fnUntraced(function* ({ stack: stackName, stageName, fqn, ...rest }) {
180
+ instrumentCommand("state.get")(Effect.fnUntraced(function* ({ stack: stackName, stageName, fqn, ...rest }) {
173
181
  yield* withStateService(rest, (state) =>
174
182
  Effect.gen(function* () {
175
183
  const value = yield* state.get({
@@ -187,13 +195,13 @@ const getCommand = Command.make(
187
195
  yield* Console.log(JSON.stringify(encodeState(value), null, 2));
188
196
  }),
189
197
  );
190
- }),
198
+ })),
191
199
  );
192
200
 
193
201
  const treeCommand = Command.make(
194
202
  "tree",
195
203
  { main, envFile, stage, profile, local: localFlag },
196
- Effect.fnUntraced(function* (args) {
204
+ instrumentCommand("state.tree")(Effect.fnUntraced(function* (args) {
197
205
  yield* withStateService(args, (state) =>
198
206
  Effect.gen(function* () {
199
207
  const stacks = [...(yield* state.listStacks())].sort();
@@ -221,7 +229,7 @@ const treeCommand = Command.make(
221
229
  }
222
230
  }),
223
231
  );
224
- }),
232
+ })),
225
233
  );
226
234
 
227
235
  const clearStackArg = Argument.string("stack").pipe(
@@ -255,7 +263,7 @@ const clearCommand = Command.make(
255
263
  local: localFlag,
256
264
  yes,
257
265
  },
258
- Effect.fnUntraced(function* ({ stack: stackOpt, stageName: stageOpt, yes: yesFlag, ...rest }) {
266
+ instrumentCommand("state.clear")(Effect.fnUntraced(function* ({ stack: stackOpt, stageName: stageOpt, yes: yesFlag, ...rest }) {
259
267
  const stackName = Option.getOrUndefined(stackOpt);
260
268
  const stageName = Option.getOrUndefined(stageOpt);
261
269
 
@@ -308,7 +316,7 @@ const clearCommand = Command.make(
308
316
  }
309
317
  }),
310
318
  );
311
- }),
319
+ })),
312
320
  );
313
321
 
314
322
  export const stateCommand = Command.make("state", {}).pipe(
@@ -18,6 +18,7 @@ import {
18
18
  envFile,
19
19
  formatLocalTimestamp,
20
20
  importStack,
21
+ instrumentCommand,
21
22
  main,
22
23
  parseResourceFilter,
23
24
  profile,
@@ -36,7 +37,14 @@ export const tailCommand = Command.make(
36
37
  profile,
37
38
  filter: resourceFilter,
38
39
  },
39
- Effect.fnUntraced(function* ({ main, stage, envFile, profile, filter }) {
40
+ instrumentCommand(
41
+ "tail",
42
+ (a: { main: string; stage: string; profile: string }) => ({
43
+ "alchemy.stage": a.stage,
44
+ "alchemy.profile": a.profile,
45
+ "alchemy.main": a.main,
46
+ }),
47
+ )(Effect.fnUntraced(function* ({ main, stage, envFile, profile, filter }) {
40
48
  const stackEffect = yield* importStack(main);
41
49
 
42
50
  const services = Layer.mergeAll(
@@ -134,5 +142,5 @@ export const tailCommand = Command.make(
134
142
  }).pipe(Stream.runForEach((line) => Console.log(line)));
135
143
  }).pipe(Effect.provide(stack.services));
136
144
  }).pipe(Effect.provide(services));
137
- }),
145
+ })),
138
146
  );
@@ -96,9 +96,9 @@ export declare const makeLSPClient: (id: string, proc: Subprocess, root: string)
96
96
  readonly character: number;
97
97
  };
98
98
  };
99
+ readonly source?: string | undefined;
99
100
  readonly code?: string | number | undefined;
100
101
  readonly severity?: number | undefined;
101
- readonly source?: string | undefined;
102
102
  }[], never, never>;
103
103
  getDiagnostics: (path: string) => Effect.Effect<never[] | {
104
104
  readonly message: string;
@@ -112,9 +112,9 @@ export declare const makeLSPClient: (id: string, proc: Subprocess, root: string)
112
112
  readonly character: number;
113
113
  };
114
114
  };
115
+ readonly source?: string | undefined;
115
116
  readonly code?: string | number | undefined;
116
117
  readonly severity?: number | undefined;
117
- readonly source?: string | undefined;
118
118
  }[], never, never>;
119
119
  onDiagnostics: (callback: (uri: string, diagnostics: Diagnostic[]) => Effect.Effect<void>) => Effect.Effect<void, never, never>;
120
120
  shutdown: Effect.Effect<void, never, never>;
@@ -1 +1 @@
1
- {"version":3,"file":"Apply.d.ts","sourceRoot":"","sources":["../src/Apply.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAQ7C,OAAO,EAGL,GAAG,EACJ,MAAM,cAAc,CAAC;AAItB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,EAAc,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAG/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EASL,KAAK,EACL,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,MAAM,WAAW,CACrB,CAAC,SAAS,IAAI,EACd,GAAG,GAAG,KAAK,EACX,GAAG,GAAG,KAAK,IACT,MAAM,CAAC,MAAM,CACf;KACG,CAAC,IAAI,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/C,EACD,GAAG,EACH,GAAG,CACJ,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,IAAI,IAAI;KACvC,EAAE,IAAI,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,SAC5C,MAAM,GACN,SAAS,GACT,KAAK,GACL,KAAK,GACL,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;CACrD,CAAC;AAwBF,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,IAAI,EAClC,MAAM,CAAC,KACN,MAAM,CAAC,MAAM,CACd,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAC1B,MAAM,CAAC,qBAAqB,GAAG,MAAM,CAAC,kBAAkB,GAAG,eAAe,EAC1E,GAAG,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CA+DC,CAAC"}
1
+ {"version":3,"file":"Apply.d.ts","sourceRoot":"","sources":["../src/Apply.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAQ7C,OAAO,EAGL,GAAG,EACJ,MAAM,cAAc,CAAC;AAItB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,EAAc,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAG/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EASL,KAAK,EACL,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,MAAM,WAAW,CACrB,CAAC,SAAS,IAAI,EACd,GAAG,GAAG,KAAK,EACX,GAAG,GAAG,KAAK,IACT,MAAM,CAAC,MAAM,CACf;KACG,CAAC,IAAI,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/C,EACD,GAAG,EACH,GAAG,CACJ,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,IAAI,IAAI;KACvC,EAAE,IAAI,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,SAC5C,MAAM,GACN,SAAS,GACT,KAAK,GACL,KAAK,GACL,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;CACrD,CAAC;AA2DF,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,IAAI,EAClC,MAAM,CAAC,KACN,MAAM,CAAC,MAAM,CACd,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAC1B,MAAM,CAAC,qBAAqB,GAAG,MAAM,CAAC,kBAAkB,GAAG,eAAe,EAC1E,GAAG,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAuE1B,CAAC"}
package/lib/Apply.js CHANGED
@@ -11,8 +11,28 @@ import {} from "./Plan.js";
11
11
  import { findProviderByType } from "./Provider.js";
12
12
  import { Stack } from "./Stack.js";
13
13
  import { Stage } from "./Stage.js";
14
+ import { recordResourceOp } from "./Telemetry/Metrics.js";
14
15
  import { State, StateStoreError, } from "./State/index.js";
15
16
  const provideLifecycleScope = (fqn, instanceId) => (effect) => Effect.serviceOption(ArtifactStore).pipe(Effect.map(Option.getOrElse(createArtifactStore)), Effect.flatMap((store) => effect.pipe(Effect.provideService(Artifacts, makeScopedArtifacts(store, fqn)), Effect.provideService(InstanceId, instanceId))));
17
+ /**
18
+ * Instruments a single provider lifecycle call with an OTel span
19
+ * (`provider.<op>`), the resource counter / duration histogram, and the
20
+ * scoped artifacts/instance services normally supplied by
21
+ * {@link provideLifecycleScope}.
22
+ *
23
+ * This is the only call site through which provider lifecycle methods
24
+ * are dispatched, so wrapping it here gives us a fully-instrumented
25
+ * toolchain without touching any individual provider implementation.
26
+ */
27
+ const instrumentLifecycle = (op, fqn, resourceType, logicalId, instanceId) => (effect) => effect.pipe(provideLifecycleScope(fqn, instanceId), recordResourceOp(resourceType, op), Effect.withSpan(`provider.${op}`, {
28
+ attributes: {
29
+ "alchemy.resource.fqn": fqn,
30
+ "alchemy.resource.type": resourceType,
31
+ "alchemy.resource.logical_id": logicalId,
32
+ "alchemy.resource.instance_id": instanceId,
33
+ "alchemy.resource.op": op,
34
+ },
35
+ }));
16
36
  export const apply = (plan) => Effect.gen(function* () {
17
37
  const cli = yield* Cli;
18
38
  const session = yield* cli.startApplySession(plan);
@@ -35,7 +55,12 @@ export const apply = (plan) => Effect.gen(function* () {
35
55
  }
36
56
  const outputs = Object.fromEntries(Object.entries(tracker).map(([fqn, t]) => [fqn, t.output]));
37
57
  return yield* Output.evaluate(plan.output, outputs);
38
- }).pipe(ensureArtifactStore);
58
+ }).pipe(ensureArtifactStore, Effect.withSpan("apply", {
59
+ attributes: {
60
+ "alchemy.resources.count": Object.keys(plan.resources).length,
61
+ "alchemy.deletions.count": Object.keys(plan.deletions).length,
62
+ },
63
+ }));
39
64
  // ── Phase 1: concurrent initial execution ──────────────────────────────────
40
65
  //
41
66
  // Each resource gets a Deferred<void> that signals "I have some output
@@ -189,7 +214,7 @@ const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state
189
214
  instanceId,
190
215
  bindings: excludeDeletedBindings(node.bindings),
191
216
  })
192
- .pipe(provideLifecycleScope(fqn, instanceId));
217
+ .pipe(instrumentLifecycle("precreate", fqn, node.resource.Type, logicalId, instanceId));
193
218
  yield* commit({
194
219
  status: "creating",
195
220
  fqn,
@@ -226,7 +251,7 @@ const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state
226
251
  session: scopedSession,
227
252
  output: attr,
228
253
  })
229
- .pipe(provideLifecycleScope(fqn, instanceId));
254
+ .pipe(instrumentLifecycle("create", fqn, node.resource.Type, logicalId, instanceId));
230
255
  yield* commit({
231
256
  status: "created",
232
257
  fqn,
@@ -307,7 +332,7 @@ const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state
307
332
  // @ts-expect-error - type system says this can be undefined, can it be?
308
333
  output: node.state.attr,
309
334
  })
310
- .pipe(provideLifecycleScope(fqn, instanceId));
335
+ .pipe(instrumentLifecycle("update", fqn, node.resource.Type, logicalId, instanceId));
311
336
  if (node.state.status === "replaced") {
312
337
  yield* commit({
313
338
  // The live replacement changed, but cleanup of older generations still
@@ -401,7 +426,7 @@ const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state
401
426
  instanceId,
402
427
  bindings: excludeDeletedBindings(node.bindings),
403
428
  })
404
- .pipe(provideLifecycleScope(fqn, instanceId));
429
+ .pipe(instrumentLifecycle("precreate", fqn, node.resource.Type, logicalId, instanceId));
405
430
  yield* commit({
406
431
  status: "replacing",
407
432
  fqn,
@@ -440,7 +465,7 @@ const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state
440
465
  session: scopedSession,
441
466
  output: attr,
442
467
  })
443
- .pipe(provideLifecycleScope(fqn, instanceId));
468
+ .pipe(instrumentLifecycle("create", fqn, node.resource.Type, logicalId, instanceId));
444
469
  yield* commit({
445
470
  // Creation of the new generation succeeded; from here on the only remaining
446
471
  // work is draining the old chain via garbage collection.
@@ -475,7 +500,14 @@ const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state
475
500
  // @ts-expect-error - node is never, this should be unreachable
476
501
  return yield* Effect.die(`Unknown action: ${node.action}`);
477
502
  });
478
- });
503
+ }).pipe(Effect.withSpan("apply.resource", {
504
+ attributes: {
505
+ "alchemy.resource.fqn": fqn,
506
+ "alchemy.resource.type": node.resource.Type,
507
+ "alchemy.resource.logical_id": node.resource.LogicalId,
508
+ "alchemy.resource.action": node.action,
509
+ },
510
+ }));
479
511
  // ── Phase 3: imperative convergence loop ───────────────────────────────────
480
512
  //
481
513
  // After the initial concurrent pass, some resources may have been created
@@ -519,7 +551,7 @@ const converge = Effect.fnUntraced(function* (plan, tracker, terminalStatuses, s
519
551
  olds: oldProps,
520
552
  output: tracker[fqn].output,
521
553
  })
522
- .pipe(provideLifecycleScope(fqn, instanceId));
554
+ .pipe(instrumentLifecycle("update", fqn, node.resource.Type, logicalId, instanceId));
523
555
  tracker[fqn] = {
524
556
  output: attr,
525
557
  props: newProps,
@@ -647,7 +679,7 @@ const collectGarbage = Effect.fnUntraced(function* (plan, session) {
647
679
  session: scopedSession,
648
680
  bindings: [],
649
681
  })
650
- .pipe(provideLifecycleScope(fqn, instanceId));
682
+ .pipe(instrumentLifecycle("delete", fqn, resourceType, logicalId, instanceId));
651
683
  }
652
684
  if (isDeleteNode(node)) {
653
685
  yield* state.delete({