@intentius/chant-lexicon-docker 0.99.0 → 0.101.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "dfa5e41a1e31a64ea60eeabfe6444b2ba58a81bf0ea14a37a3e092301ddee648",
4
+ "manifest.json": "d1dd1b11e3d31c5da985b63fc04b567c81c063af6ac1d4da3d7766c88b740788",
5
5
  "meta.json": "2996be8fdb14f3f935e1e1441fed8f69af7b6aeeefb14dac96301cc41eba8046",
6
6
  "types/index.d.ts": "7b2e6f784b7dbecbbe7924fa1aec7e2520ac0547069311253a006bc2267a5646",
7
7
  "rules/no-latest-tag.ts": "efd060453d8ca3d5b85c3972906c6650b6f63eb5f67cb6cdb0f16a41cad91228",
@@ -15,5 +15,5 @@
15
15
  "skills/chant-docker.md": "3ff0708e3c76e245f202948949c768464563af2b60cc9aa2ca52f494ef8357f1",
16
16
  "skills/chant-docker-patterns.md": "ad1b0196d8150b2df579a699ff107f604340c799c04f3460ebf65003e287b12a"
17
17
  },
18
- "composite": "962c32558dea3b244bd1c873b0c309fcb79198625cb691c44dc531b6e6aceeef"
18
+ "composite": "e4c7105a3d96f148222aa308477aea6191dbb466263f0abd60b750263ded3b3d"
19
19
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docker",
3
- "version": "0.99.0",
3
+ "version": "0.101.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Docker",
6
6
  "intrinsics": [
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AAqPhF,eAAO,MAAM,gBAAgB,EAAE,UAoE9B,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAoB,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AA+RlG,eAAO,MAAM,gBAAgB,EAAE,UAqE9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-docker",
3
- "version": "0.99.0",
3
+ "version": "0.101.0",
4
4
  "description": "Docker lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -70,7 +70,7 @@
70
70
  "typescript": "^5.9.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@intentius/chant": "^0.99.0",
73
+ "@intentius/chant": "^0.101.0",
74
74
  "typescript": "^5.9.3"
75
75
  }
76
76
  }
@@ -317,3 +317,52 @@ describe("dockerSerializer.serialize — name gen", () => {
317
317
  expect(output).toContain("myWebServer:");
318
318
  });
319
319
  });
320
+
321
+ // ── Telemetry attribution (#2558, D22) ─────────────────────────────
322
+
323
+ describe("telemetry attribution", () => {
324
+ const run = (props: Record<string, unknown>, telemetry?: { workspace?: string; member?: string; environment?: string }) => {
325
+ const result = dockerSerializer.serialize(new Map([["api", new MockService(props)]]), [], telemetry ? { telemetry } : undefined);
326
+ return typeof result === "string" ? result : result.primary;
327
+ };
328
+ const attrs = { workspace: "acme", member: "delivery", environment: "prod" };
329
+
330
+ test("a build with no telemetry context is the same bytes as one that passes an empty context", () => {
331
+ const props = { image: "nginx:1" };
332
+ expect(run(props)).toBe(dockerSerializer.serialize(new Map([["api", new MockService(props)]]), [], {}) as string);
333
+ expect(run(props)).not.toContain("OTEL_");
334
+ });
335
+
336
+ test("stamps the service name and the resource attributes into the environment", () => {
337
+ const out = run({ image: "nginx:1" }, attrs);
338
+ expect(out).toContain("OTEL_SERVICE_NAME: api");
339
+ expect(out).toContain("OTEL_RESOURCE_ATTRIBUTES: chant.workspace=acme,chant.member=delivery,chant.decl=api,deployment.environment.name=prod");
340
+ });
341
+
342
+ test("leaves out an attribute the build does not know", () => {
343
+ const out = run({ image: "nginx:1" }, { workspace: "acme" });
344
+ expect(out).toContain("OTEL_RESOURCE_ATTRIBUTES: chant.workspace=acme,chant.decl=api");
345
+ expect(out).not.toContain("chant.member");
346
+ expect(out).not.toContain("deployment.environment.name");
347
+ });
348
+
349
+ test("stamps service.version when the image is pinned by digest", () => {
350
+ const out = run({ image: "ghcr.io/acme/api@sha256:abc123" }, attrs);
351
+ expect(out).toContain("service.version=sha256%3Aabc123");
352
+ });
353
+
354
+ test("keeps the keys a map environment already sets and appends the missing attributes", () => {
355
+ const out = run({ image: "x", environment: { OTEL_SERVICE_NAME: "checkout", OTEL_RESOURCE_ATTRIBUTES: "chant.member=mine,team=pay", PORT: "80" } }, attrs);
356
+ expect(out).toContain("OTEL_SERVICE_NAME: checkout");
357
+ expect(out).toContain("OTEL_RESOURCE_ATTRIBUTES: chant.member=mine,team=pay,chant.workspace=acme,chant.decl=api,deployment.environment.name=prod");
358
+ expect(out).toContain("PORT:");
359
+ });
360
+
361
+ test("adds the variables to a list environment, keeping an entry already there", () => {
362
+ const out = run({ image: "x", environment: ["PORT=80", "OTEL_SERVICE_NAME=checkout"] }, attrs);
363
+ expect(out).toContain("- PORT=80");
364
+ expect(out).toContain("- OTEL_SERVICE_NAME=checkout");
365
+ expect(out).not.toContain("OTEL_SERVICE_NAME=api");
366
+ expect(out).toContain("- OTEL_RESOURCE_ATTRIBUTES=chant.workspace=acme,chant.member=delivery");
367
+ });
368
+ });
package/src/serializer.ts CHANGED
@@ -11,7 +11,8 @@
11
11
 
12
12
  import type { Declarable } from "@intentius/chant/declarable";
13
13
  import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
14
- import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
14
+ import type { SerializeContext, Serializer, SerializerResult } from "@intentius/chant/serializer";
15
+ import { mergeResourceAttributes, telemetryEnvironment, type TelemetryAttribution } from "@intentius/chant/telemetry-attribution";
15
16
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
16
17
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
17
18
  import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
@@ -97,6 +98,45 @@ function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unkn
97
98
  return walkValue(preprocessed, entityNames, dockerVisitor(entityNames));
98
99
  }
99
100
 
101
+ // ── Telemetry attribution (#2558, D22) ────────────────────────────
102
+
103
+ /** The digest of an image reference pinned with `@sha256:...`, or undefined. */
104
+ function imageDigest(image: unknown): string | undefined {
105
+ if (typeof image !== "string") return undefined;
106
+ const at = image.lastIndexOf("@");
107
+ return at === -1 ? undefined : image.slice(at + 1);
108
+ }
109
+
110
+ /**
111
+ * Stamp one service's attributes as `OTEL_SERVICE_NAME` and
112
+ * `OTEL_RESOURCE_ATTRIBUTES` in its environment. A value the service already
113
+ * sets is kept: its own `OTEL_SERVICE_NAME` wins, and its own resource
114
+ * attributes keep their keys while the missing ones are appended.
115
+ */
116
+ function stampTelemetry(service: Record<string, unknown>, name: string, telemetry: TelemetryAttribution): void {
117
+ const version = imageDigest(service.image);
118
+ const vars = telemetryEnvironment(telemetry, { service: name, decl: name, ...(version ? { version } : {}) });
119
+ const env = service.environment;
120
+ if (Array.isArray(env)) {
121
+ const entries = env.map(String);
122
+ const has = (key: string) => entries.findIndex((e) => e === key || e.startsWith(`${key}=`));
123
+ if (has("OTEL_SERVICE_NAME") === -1) entries.push(`OTEL_SERVICE_NAME=${vars.OTEL_SERVICE_NAME}`);
124
+ const at = has("OTEL_RESOURCE_ATTRIBUTES");
125
+ if (at === -1) entries.push(`OTEL_RESOURCE_ATTRIBUTES=${vars.OTEL_RESOURCE_ATTRIBUTES}`);
126
+ else if (entries[at]!.includes("=")) {
127
+ entries[at] = `OTEL_RESOURCE_ATTRIBUTES=${mergeResourceAttributes(entries[at]!.slice(entries[at]!.indexOf("=") + 1), vars.OTEL_RESOURCE_ATTRIBUTES)}`;
128
+ }
129
+ service.environment = entries;
130
+ return;
131
+ }
132
+ const map: Record<string, unknown> = env && typeof env === "object" ? { ...(env as Record<string, unknown>) } : {};
133
+ if (!("OTEL_SERVICE_NAME" in map)) map.OTEL_SERVICE_NAME = vars.OTEL_SERVICE_NAME;
134
+ const own = map.OTEL_RESOURCE_ATTRIBUTES;
135
+ if (own === undefined) map.OTEL_RESOURCE_ATTRIBUTES = vars.OTEL_RESOURCE_ATTRIBUTES;
136
+ else if (typeof own === "string") map.OTEL_RESOURCE_ATTRIBUTES = mergeResourceAttributes(own, vars.OTEL_RESOURCE_ATTRIBUTES);
137
+ service.environment = map;
138
+ }
139
+
100
140
  // ── Compose serialization ─────────────────────────────────────────
101
141
 
102
142
  function serializeCompose(
@@ -107,6 +147,7 @@ function serializeCompose(
107
147
  secrets: Map<string, Declarable>,
108
148
  defaultLabels: Record<string, string>,
109
149
  entityNames: Map<Declarable, string>,
150
+ telemetry?: TelemetryAttribution,
110
151
  ): string {
111
152
  const doc: Record<string, unknown> = {};
112
153
 
@@ -124,6 +165,7 @@ function serializeCompose(
124
165
  for (const [k, v] of Object.entries(props)) {
125
166
  if (v !== undefined && v !== null) cleaned[k] = v;
126
167
  }
168
+ if (telemetry) stampTelemetry(cleaned, name, telemetry);
127
169
  servicesSection[name] = cleaned;
128
170
  }
129
171
  doc.services = servicesSection;
@@ -263,6 +305,7 @@ export const dockerSerializer: Serializer = {
263
305
  serialize(
264
306
  entities: Map<string, Declarable>,
265
307
  _outputs?: LexiconOutput[],
308
+ context?: SerializeContext,
266
309
  ): string | SerializerResult {
267
310
  const entityNames = new Map<Declarable, string>();
268
311
  for (const [name, entity] of entities) {
@@ -310,7 +353,7 @@ export const dockerSerializer: Serializer = {
310
353
 
311
354
  const composeYaml = serializeCompose(
312
355
  services, volumes, networks, configs, secrets,
313
- defaultLabels, entityNames,
356
+ defaultLabels, entityNames, context?.telemetry,
314
357
  );
315
358
 
316
359
  if (dockerfiles.size === 0) {