alchemy 2.0.0-beta.17 → 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/AWS/CloudFront/CachePolicy.d.ts +106 -0
- package/lib/AWS/CloudFront/CachePolicy.d.ts.map +1 -0
- package/lib/AWS/CloudFront/CachePolicy.js +147 -0
- package/lib/AWS/CloudFront/CachePolicy.js.map +1 -0
- package/lib/AWS/CloudFront/KeyGroup.d.ts +72 -0
- package/lib/AWS/CloudFront/KeyGroup.d.ts.map +1 -0
- package/lib/AWS/CloudFront/KeyGroup.js +136 -0
- package/lib/AWS/CloudFront/KeyGroup.js.map +1 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts +84 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts.map +1 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.js +139 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.js.map +1 -0
- package/lib/AWS/CloudFront/PublicKey.d.ts +72 -0
- package/lib/AWS/CloudFront/PublicKey.d.ts.map +1 -0
- package/lib/AWS/CloudFront/PublicKey.js +137 -0
- package/lib/AWS/CloudFront/PublicKey.js.map +1 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts +114 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts.map +1 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.js +162 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.js.map +1 -0
- package/lib/AWS/CloudFront/index.d.ts +5 -0
- package/lib/AWS/CloudFront/index.d.ts.map +1 -1
- package/lib/AWS/CloudFront/index.js +5 -0
- package/lib/AWS/CloudFront/index.js.map +1 -1
- package/lib/AWS/ECS/CapacityProvider.d.ts +102 -0
- package/lib/AWS/ECS/CapacityProvider.d.ts.map +1 -0
- package/lib/AWS/ECS/CapacityProvider.js +217 -0
- package/lib/AWS/ECS/CapacityProvider.js.map +1 -0
- package/lib/AWS/ECS/index.d.ts +1 -0
- package/lib/AWS/ECS/index.d.ts.map +1 -1
- package/lib/AWS/ECS/index.js +1 -0
- package/lib/AWS/ECS/index.js.map +1 -1
- package/lib/AWS/Providers.d.ts.map +1 -1
- package/lib/AWS/Providers.js +7 -1
- package/lib/AWS/Providers.js.map +1 -1
- 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/GitHub/Secrets.d.ts +45 -0
- package/lib/GitHub/Secrets.d.ts.map +1 -0
- package/lib/GitHub/Secrets.js +30 -0
- package/lib/GitHub/Secrets.js.map +1 -0
- package/lib/GitHub/index.d.ts +1 -0
- package/lib/GitHub/index.d.ts.map +1 -1
- package/lib/GitHub/index.js +1 -0
- package/lib/GitHub/index.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/AWS/CloudFront/CachePolicy.ts +271 -0
- package/src/AWS/CloudFront/KeyGroup.ts +233 -0
- package/src/AWS/CloudFront/OriginRequestPolicy.ts +256 -0
- package/src/AWS/CloudFront/PublicKey.ts +245 -0
- package/src/AWS/CloudFront/ResponseHeadersPolicy.ts +308 -0
- package/src/AWS/CloudFront/index.ts +11 -0
- package/src/AWS/ECS/CapacityProvider.ts +336 -0
- package/src/AWS/ECS/index.ts +4 -0
- package/src/AWS/Providers.ts +12 -0
- 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/GitHub/Secrets.ts +66 -0
- package/src/GitHub/index.ts +1 -0
- 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
package/src/GitHub/index.ts
CHANGED
package/src/Output.ts
CHANGED
|
@@ -353,7 +353,8 @@ function proxy(self: any): any {
|
|
|
353
353
|
prop === ExprSymbol || prop === inspect ? true : prop in self,
|
|
354
354
|
get: (target, prop) =>
|
|
355
355
|
prop === Symbol.toPrimitive
|
|
356
|
-
? (hint: string) =>
|
|
356
|
+
? (hint: string) =>
|
|
357
|
+
hint === "number" ? Number.NaN : self.toString()
|
|
357
358
|
: prop === ExprSymbol
|
|
358
359
|
? self
|
|
359
360
|
: prop === inspect
|
package/src/Plan.ts
CHANGED
|
@@ -397,7 +397,7 @@ export const make = <A>(
|
|
|
397
397
|
const resourceGraph = Object.fromEntries(
|
|
398
398
|
(yield* Effect.all(
|
|
399
399
|
resources.map(
|
|
400
|
-
Effect.fn(function* (resource) {
|
|
400
|
+
Effect.fn("plan.diff.resource")(function* (resource) {
|
|
401
401
|
const provider = yield* findProviderByType(resource.Type);
|
|
402
402
|
const id = resource.LogicalId;
|
|
403
403
|
const fqn = resource.FQN;
|
|
@@ -723,8 +723,7 @@ export const make = <A>(
|
|
|
723
723
|
const deletions = Object.fromEntries(
|
|
724
724
|
(yield* Effect.all(
|
|
725
725
|
(yield* state.list({ stack: stackName, stage: stage })).map(
|
|
726
|
-
Effect.fn(function* (fqn) {
|
|
727
|
-
// Check if this FQN is in the new resources
|
|
726
|
+
Effect.fn("plan.diff.deletion")(function* (fqn) {
|
|
728
727
|
if (newResourceFqns.has(fqn)) {
|
|
729
728
|
return;
|
|
730
729
|
}
|
|
@@ -805,7 +804,17 @@ export const make = <A>(
|
|
|
805
804
|
deletions,
|
|
806
805
|
output: stack.output,
|
|
807
806
|
} satisfies Plan<A> as Plan<A>;
|
|
808
|
-
}).pipe(
|
|
807
|
+
}).pipe(
|
|
808
|
+
ensureArtifactStore,
|
|
809
|
+
Effect.withSpan("plan.make", {
|
|
810
|
+
attributes: {
|
|
811
|
+
"alchemy.stack": stack.name,
|
|
812
|
+
"alchemy.stage": stack.stage,
|
|
813
|
+
"alchemy.resources.count": Object.keys(stack.resources).length,
|
|
814
|
+
"alchemy.force": !!options.force,
|
|
815
|
+
},
|
|
816
|
+
}),
|
|
817
|
+
);
|
|
809
818
|
|
|
810
819
|
const providePlanScope =
|
|
811
820
|
(fqn: string, instanceId: string) =>
|
package/src/Resource.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as Option from "effect/Option";
|
|
|
3
3
|
import { pipeArguments, type Pipeable } from "effect/Pipeable";
|
|
4
4
|
import { SingleShotGen } from "effect/Utils";
|
|
5
5
|
import { toFqn } from "./FQN.ts";
|
|
6
|
-
import type { Input } from "./Input.ts";
|
|
6
|
+
import type { Input, InputProps } from "./Input.ts";
|
|
7
7
|
import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts";
|
|
8
8
|
import * as Output from "./Output.ts";
|
|
9
9
|
import { Provider } from "./Provider.ts";
|
|
@@ -33,7 +33,7 @@ export type ResourceConstructor<R extends ResourceLike, Req = never> = {
|
|
|
33
33
|
): Effect.Effect<R, never, Req>;
|
|
34
34
|
<PropsReq = never>(
|
|
35
35
|
id: string,
|
|
36
|
-
props: Effect.Effect<
|
|
36
|
+
props: Effect.Effect<InputProps<R["Props"]>, never, PropsReq>,
|
|
37
37
|
): Effect.Effect<R, never, PropsReq | Req>;
|
|
38
38
|
};
|
|
39
39
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as nodePath from "node:path";
|
|
6
|
+
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
|
|
9
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
10
|
+
|
|
11
|
+
const ALCHEMY_DIR = nodePath.join(os.homedir(), ".alchemy");
|
|
12
|
+
const ID_PATH = nodePath.join(ALCHEMY_DIR, "id");
|
|
13
|
+
const DISABLED_PATH = nodePath.join(ALCHEMY_DIR, "telemetry-disabled");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OTel resource attributes describing the user, project, runtime, and
|
|
17
|
+
* environment running the alchemy CLI. Computed once per process and
|
|
18
|
+
* attached to every span and metric exported by `TelemetryLive`.
|
|
19
|
+
*/
|
|
20
|
+
export interface TelemetryAttributes {
|
|
21
|
+
readonly "alchemy.user.id": string;
|
|
22
|
+
readonly "alchemy.session.id": string;
|
|
23
|
+
readonly "alchemy.version": string;
|
|
24
|
+
readonly "alchemy.git.root_commit": string;
|
|
25
|
+
readonly "alchemy.git.origin_hash": string;
|
|
26
|
+
readonly "alchemy.git.branch_hash": string;
|
|
27
|
+
readonly "alchemy.runtime.name": string;
|
|
28
|
+
readonly "alchemy.runtime.version": string;
|
|
29
|
+
readonly "alchemy.ci.provider": string;
|
|
30
|
+
readonly "alchemy.ci": boolean;
|
|
31
|
+
readonly "host.arch": string;
|
|
32
|
+
readonly "os.type": string;
|
|
33
|
+
readonly "os.version": string;
|
|
34
|
+
readonly "host.cpus": number;
|
|
35
|
+
readonly "host.memory_mb": number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tryRead = (path: string): Effect.Effect<string | null> =>
|
|
39
|
+
Effect.tryPromise({
|
|
40
|
+
try: () => fs.readFile(path, "utf-8").then((s) => s.trim()),
|
|
41
|
+
catch: () => null as never,
|
|
42
|
+
}).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
43
|
+
|
|
44
|
+
const sha256Hex = (input: string): string =>
|
|
45
|
+
crypto.createHash("sha256").update(input).digest("hex");
|
|
46
|
+
|
|
47
|
+
const hashStringOrEmpty = (input: string | null): string =>
|
|
48
|
+
input == null || input === "" ? "" : sha256Hex(input);
|
|
49
|
+
|
|
50
|
+
const execCapture = (cmd: string): Effect.Effect<string | null> =>
|
|
51
|
+
Effect.callback<string | null>((resume) => {
|
|
52
|
+
exec(cmd, { timeout: 1500 }, (err, stdout) => {
|
|
53
|
+
if (err) {
|
|
54
|
+
resume(Effect.succeed(null));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
resume(Effect.succeed(stdout.toString().trim()));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const getOrCreateUserId: Effect.Effect<string> = Effect.gen(function* () {
|
|
62
|
+
const existing = yield* tryRead(ID_PATH);
|
|
63
|
+
if (existing) return existing;
|
|
64
|
+
|
|
65
|
+
const id = crypto.randomUUID();
|
|
66
|
+
yield* Effect.tryPromise({
|
|
67
|
+
try: async () => {
|
|
68
|
+
await fs.mkdir(ALCHEMY_DIR, { recursive: true });
|
|
69
|
+
await fs.writeFile(ID_PATH, id);
|
|
70
|
+
},
|
|
71
|
+
catch: () => null as never,
|
|
72
|
+
}).pipe(Effect.catch(() => Effect.void));
|
|
73
|
+
return id;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const getRootCommitHash: Effect.Effect<string | null> = execCapture(
|
|
77
|
+
process.platform === "win32"
|
|
78
|
+
? `git rev-list --max-parents=0 HEAD | ForEach-Object { if (-not (git cat-file -p $_ | Select-String "^parent ")) { $_ } }`
|
|
79
|
+
: `git rev-list --max-parents=0 HEAD | xargs -r -I{} sh -c 'git cat-file -p {} | grep -q "^parent " || echo {}'`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const getGitOriginUrl: Effect.Effect<string | null> = execCapture(
|
|
83
|
+
"git config --get remote.origin.url",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const getBranchName: Effect.Effect<string | null> = execCapture(
|
|
87
|
+
"git rev-parse --abbrev-ref HEAD",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const getRuntime = (): { name: string; version: string } => {
|
|
91
|
+
const g = globalThis as unknown as {
|
|
92
|
+
Bun?: { version: string };
|
|
93
|
+
Deno?: { version?: { deno?: string } };
|
|
94
|
+
};
|
|
95
|
+
if (typeof g.Bun !== "undefined") {
|
|
96
|
+
return { name: "bun", version: g.Bun.version };
|
|
97
|
+
}
|
|
98
|
+
if (typeof g.Deno !== "undefined") {
|
|
99
|
+
return { name: "deno", version: g.Deno.version?.deno ?? "" };
|
|
100
|
+
}
|
|
101
|
+
if (globalThis.process?.versions?.node) {
|
|
102
|
+
return { name: "node", version: process.versions.node };
|
|
103
|
+
}
|
|
104
|
+
return { name: "", version: "" };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const CI_PROVIDERS = [
|
|
108
|
+
{ env: "GITHUB_ACTIONS", provider: "GitHub Actions", isCI: true },
|
|
109
|
+
{ env: "GITLAB_CI", provider: "GitLab CI", isCI: true },
|
|
110
|
+
{ env: "CIRCLECI", provider: "CircleCI", isCI: true },
|
|
111
|
+
{ env: "JENKINS_URL", provider: "Jenkins", isCI: true },
|
|
112
|
+
{ env: "TRAVIS", provider: "Travis CI", isCI: true },
|
|
113
|
+
{ env: "BUILDKITE", provider: "Buildkite", isCI: true },
|
|
114
|
+
{ env: "NOW_BUILDER", provider: "Vercel", isCI: true },
|
|
115
|
+
{ env: "VERCEL", provider: "Vercel", isCI: false },
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
const getCIEnvironment = (): { provider: string; isCI: boolean } => {
|
|
119
|
+
for (const p of CI_PROVIDERS) {
|
|
120
|
+
if (process.env[p.env]) {
|
|
121
|
+
return { provider: p.provider, isCI: p.isCI };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { provider: "", isCI: !!process.env.CI };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const TELEMETRY_DISABLED_ENV = (): boolean =>
|
|
128
|
+
!!process.env.ALCHEMY_TELEMETRY_DISABLED ||
|
|
129
|
+
!!process.env.DO_NOT_TRACK ||
|
|
130
|
+
!!process.env.NO_TRACK;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `true` if telemetry is disabled via environment variable
|
|
134
|
+
* (`DO_NOT_TRACK`, `NO_TRACK`, `ALCHEMY_TELEMETRY_DISABLED`) or via a
|
|
135
|
+
* persisted opt-out file at `~/.alchemy/telemetry-disabled`.
|
|
136
|
+
*/
|
|
137
|
+
export const isTelemetryDisabled: Effect.Effect<boolean> = Effect.gen(
|
|
138
|
+
function* () {
|
|
139
|
+
if (TELEMETRY_DISABLED_ENV()) return true;
|
|
140
|
+
const persisted = yield* tryRead(DISABLED_PATH);
|
|
141
|
+
return persisted === "true";
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Persists an opt-out so future invocations skip telemetry without needing
|
|
147
|
+
* an env var.
|
|
148
|
+
*/
|
|
149
|
+
export const setTelemetryDisabled: Effect.Effect<void> = Effect.tryPromise({
|
|
150
|
+
try: async () => {
|
|
151
|
+
await fs.mkdir(ALCHEMY_DIR, { recursive: true });
|
|
152
|
+
await fs.writeFile(DISABLED_PATH, "true");
|
|
153
|
+
},
|
|
154
|
+
catch: () => null as never,
|
|
155
|
+
}).pipe(Effect.catch(() => Effect.void));
|
|
156
|
+
|
|
157
|
+
export const setTelemetryEnabled: Effect.Effect<void> = Effect.tryPromise({
|
|
158
|
+
try: () => fs.rm(DISABLED_PATH, { force: true }),
|
|
159
|
+
catch: () => null as never,
|
|
160
|
+
}).pipe(Effect.catch(() => Effect.void));
|
|
161
|
+
|
|
162
|
+
const collectAttributesUncached: Effect.Effect<TelemetryAttributes> =
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const [userId, rootCommit, originUrl, branch] = yield* Effect.all(
|
|
165
|
+
[
|
|
166
|
+
getOrCreateUserId,
|
|
167
|
+
getRootCommitHash,
|
|
168
|
+
getGitOriginUrl,
|
|
169
|
+
getBranchName,
|
|
170
|
+
] as const,
|
|
171
|
+
{ concurrency: "unbounded" },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const runtime = getRuntime();
|
|
175
|
+
const ci = getCIEnvironment();
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"alchemy.user.id": userId,
|
|
179
|
+
"alchemy.session.id":
|
|
180
|
+
process.env.ALCHEMY_TELEMETRY_SESSION_ID ?? crypto.randomUUID(),
|
|
181
|
+
"alchemy.version": packageJson.version,
|
|
182
|
+
"alchemy.git.root_commit": rootCommit ?? "",
|
|
183
|
+
"alchemy.git.origin_hash": hashStringOrEmpty(originUrl),
|
|
184
|
+
"alchemy.git.branch_hash": hashStringOrEmpty(branch),
|
|
185
|
+
"alchemy.runtime.name": runtime.name,
|
|
186
|
+
"alchemy.runtime.version": runtime.version,
|
|
187
|
+
"alchemy.ci.provider": ci.provider,
|
|
188
|
+
"alchemy.ci": ci.isCI,
|
|
189
|
+
"host.arch": os.arch(),
|
|
190
|
+
"os.type": os.platform(),
|
|
191
|
+
"os.version": os.release(),
|
|
192
|
+
"host.cpus": os.cpus().length,
|
|
193
|
+
"host.memory_mb": Math.round(os.totalmem() / 1024 / 1024),
|
|
194
|
+
} satisfies TelemetryAttributes;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let cachedAttrs: TelemetryAttributes | undefined;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolves the {@link TelemetryAttributes} for the current process. Cached
|
|
201
|
+
* so file I/O and `git` invocations only run once per CLI invocation; every
|
|
202
|
+
* subsequent caller gets the same record (and therefore the same session
|
|
203
|
+
* id).
|
|
204
|
+
*/
|
|
205
|
+
export const collectAttributes: Effect.Effect<TelemetryAttributes> =
|
|
206
|
+
Effect.suspend(() =>
|
|
207
|
+
cachedAttrs !== undefined
|
|
208
|
+
? Effect.succeed(cachedAttrs)
|
|
209
|
+
: collectAttributesUncached.pipe(
|
|
210
|
+
Effect.tap((a) =>
|
|
211
|
+
Effect.sync(() => {
|
|
212
|
+
cachedAttrs = a;
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
4
|
+
import * as OtlpMetrics from "effect/unstable/observability/OtlpMetrics";
|
|
5
|
+
import * as OtlpSerialization from "effect/unstable/observability/OtlpSerialization";
|
|
6
|
+
import * as OtlpTracer from "effect/unstable/observability/OtlpTracer";
|
|
7
|
+
|
|
8
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
9
|
+
|
|
10
|
+
import { collectAttributes, isTelemetryDisabled } from "./Attributes.ts";
|
|
11
|
+
|
|
12
|
+
const TRACES_URL = "https://otel.alchemy.run/v1/traces";
|
|
13
|
+
const METRICS_URL = "https://otel.alchemy.run/v1/metrics";
|
|
14
|
+
|
|
15
|
+
const SERVICE_NAME = "alchemy-cli";
|
|
16
|
+
|
|
17
|
+
const buildOtlpLayer = (
|
|
18
|
+
attrs: Record<string, unknown>,
|
|
19
|
+
): Layer.Layer<never, never, never> => {
|
|
20
|
+
const resource = {
|
|
21
|
+
serviceName: SERVICE_NAME,
|
|
22
|
+
serviceVersion: packageJson.version,
|
|
23
|
+
attributes: attrs,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Short export intervals so even sub-second CLI invocations flush at
|
|
27
|
+
// least one batch before the process exits.
|
|
28
|
+
const tracer = OtlpTracer.layer({
|
|
29
|
+
url: TRACES_URL,
|
|
30
|
+
resource,
|
|
31
|
+
exportInterval: "1 second",
|
|
32
|
+
});
|
|
33
|
+
const metrics = OtlpMetrics.layer({
|
|
34
|
+
url: METRICS_URL,
|
|
35
|
+
resource,
|
|
36
|
+
exportInterval: "1 second",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return Layer.mergeAll(tracer, metrics).pipe(
|
|
40
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
41
|
+
Layer.provide(FetchHttpClient.layer),
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The CLI's telemetry layer. Builds an OTLP HTTP exporter that ships spans
|
|
47
|
+
* to {@link TRACES_URL} and metrics to {@link METRICS_URL}, attaching
|
|
48
|
+
* {@link collectAttributes} as resource-level attributes so every signal
|
|
49
|
+
* carries user/project/runtime context.
|
|
50
|
+
*
|
|
51
|
+
* If the user has opted out (via `DO_NOT_TRACK`, `NO_TRACK`,
|
|
52
|
+
* `ALCHEMY_TELEMETRY_DISABLED`, or `~/.alchemy/telemetry-disabled`), this
|
|
53
|
+
* resolves to {@link Layer.empty}. Effect's default `Tracer` is a no-op,
|
|
54
|
+
* so all `withSpan`/`Effect.fn` instrumentation in core stays free.
|
|
55
|
+
*/
|
|
56
|
+
export const TelemetryLive: Layer.Layer<never, never, never> =
|
|
57
|
+
Layer.unwrap(
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
if (yield* isTelemetryDisabled) {
|
|
60
|
+
return Layer.empty;
|
|
61
|
+
}
|
|
62
|
+
const attrs = yield* collectAttributes;
|
|
63
|
+
return buildOtlpLayer(attrs as unknown as Record<string, unknown>);
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Metric from "effect/Metric";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Counter for resource lifecycle operations. Tagged per call with
|
|
8
|
+
* `resource_type`, `op` (`precreate`/`create`/`update`/`delete`/`read`),
|
|
9
|
+
* and `status` (`success`/`error`).
|
|
10
|
+
*/
|
|
11
|
+
export const resourceCounter = Metric.counter("alchemy.resource.operations", {
|
|
12
|
+
description:
|
|
13
|
+
"Number of resource lifecycle operations dispatched by alchemy apply.",
|
|
14
|
+
incremental: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Histogram of how long each lifecycle operation takes.
|
|
19
|
+
*/
|
|
20
|
+
export const resourceDuration = Metric.timer("alchemy.resource.duration", {
|
|
21
|
+
description:
|
|
22
|
+
"Wall-clock duration of resource lifecycle operations dispatched by alchemy apply.",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Counter for CLI command invocations.
|
|
27
|
+
*/
|
|
28
|
+
export const cliCounter = Metric.counter("alchemy.cli.invocations", {
|
|
29
|
+
description: "Number of alchemy CLI command invocations.",
|
|
30
|
+
incremental: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wraps a resource lifecycle Effect to record a counter + timer entry,
|
|
35
|
+
* tagged with `resource_type`, `op`, and `status` (`success`/`error`).
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* ```ts
|
|
39
|
+
* provider.create(input).pipe(recordResourceOp(node.resource.Type, "create"))
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const recordResourceOp =
|
|
43
|
+
(resourceType: string, op: ResourceOp) =>
|
|
44
|
+
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
45
|
+
Effect.suspend(() => {
|
|
46
|
+
const startNs = process.hrtime.bigint();
|
|
47
|
+
const baseAttrs = { resource_type: resourceType, op } as const;
|
|
48
|
+
return self.pipe(
|
|
49
|
+
Effect.onExit((exit) =>
|
|
50
|
+
recordOutcome(
|
|
51
|
+
baseAttrs,
|
|
52
|
+
Exit.isSuccess(exit) ? "success" : "error",
|
|
53
|
+
elapsed(startNs),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wraps a CLI command Effect to bump {@link cliCounter} with the
|
|
61
|
+
* outcome (`success`/`error`).
|
|
62
|
+
*/
|
|
63
|
+
export const recordCli =
|
|
64
|
+
(command: string) =>
|
|
65
|
+
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
66
|
+
self.pipe(
|
|
67
|
+
Effect.onExit((exit) =>
|
|
68
|
+
Metric.update(
|
|
69
|
+
Metric.withAttributes(cliCounter, {
|
|
70
|
+
command,
|
|
71
|
+
status: Exit.isSuccess(exit) ? "success" : "error",
|
|
72
|
+
}),
|
|
73
|
+
1,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export type ResourceOp =
|
|
79
|
+
| "precreate"
|
|
80
|
+
| "create"
|
|
81
|
+
| "update"
|
|
82
|
+
| "delete"
|
|
83
|
+
| "read";
|
|
84
|
+
|
|
85
|
+
const elapsed = (startNs: bigint): Duration.Duration =>
|
|
86
|
+
Duration.nanos(process.hrtime.bigint() - startNs);
|
|
87
|
+
|
|
88
|
+
const recordOutcome = (
|
|
89
|
+
baseAttrs: { resource_type: string; op: ResourceOp },
|
|
90
|
+
status: "success" | "error",
|
|
91
|
+
duration: Duration.Duration,
|
|
92
|
+
): Effect.Effect<void> => {
|
|
93
|
+
const attrs = { ...baseAttrs, status };
|
|
94
|
+
return Effect.all(
|
|
95
|
+
[
|
|
96
|
+
Metric.update(Metric.withAttributes(resourceCounter, attrs), 1),
|
|
97
|
+
Metric.update(Metric.withAttributes(resourceDuration, attrs), duration),
|
|
98
|
+
],
|
|
99
|
+
{ discard: true },
|
|
100
|
+
);
|
|
101
|
+
};
|