@intentius/chant-lexicon-otel 0.124.46 → 0.124.49

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 (211) hide show
  1. package/README.md +2 -2
  2. package/dist/build/capability-plugin.d.ts +10 -0
  3. package/dist/build/capability-plugin.d.ts.map +1 -0
  4. package/dist/build/capability-plugin.js +19 -0
  5. package/dist/build/ocb-build.d.ts +38 -0
  6. package/dist/build/ocb-build.d.ts.map +1 -0
  7. package/dist/build/ocb-build.js +49 -0
  8. package/dist/clickhouse-tables.d.ts +73 -0
  9. package/dist/clickhouse-tables.d.ts.map +1 -0
  10. package/dist/clickhouse-tables.js +199 -0
  11. package/dist/codegen/docs.d.ts.map +1 -1
  12. package/dist/codegen/docs.js +4 -2
  13. package/dist/codegen/generate.d.ts +29 -10
  14. package/dist/codegen/generate.d.ts.map +1 -1
  15. package/dist/codegen/generate.js +82 -17
  16. package/dist/components/common.d.ts +3 -1
  17. package/dist/components/common.d.ts.map +1 -1
  18. package/dist/components/common.js +2 -0
  19. package/dist/components/connectors.d.ts +25 -90
  20. package/dist/components/connectors.d.ts.map +1 -1
  21. package/dist/components/connectors.js +3 -0
  22. package/dist/components/exporters.d.ts +78 -34
  23. package/dist/components/exporters.d.ts.map +1 -1
  24. package/dist/components/exporters.js +26 -1
  25. package/dist/components/extensions.d.ts +16 -33
  26. package/dist/components/extensions.d.ts.map +1 -1
  27. package/dist/components/filtering.d.ts +13 -60
  28. package/dist/components/filtering.d.ts.map +1 -1
  29. package/dist/components/k8s-receivers.d.ts +5 -53
  30. package/dist/components/k8s-receivers.d.ts.map +1 -1
  31. package/dist/components/overlay.d.ts +25 -0
  32. package/dist/components/overlay.d.ts.map +1 -0
  33. package/dist/components/overlay.js +19 -0
  34. package/dist/components/processors.d.ts +18 -79
  35. package/dist/components/processors.d.ts.map +1 -1
  36. package/dist/components/receivers.d.ts +17 -71
  37. package/dist/components/receivers.d.ts.map +1 -1
  38. package/dist/components/receivers.js +3 -1
  39. package/dist/components/sampling.d.ts +19 -35
  40. package/dist/components/sampling.d.ts.map +1 -1
  41. package/dist/config-hygiene.d.ts +10 -8
  42. package/dist/config-hygiene.d.ts.map +1 -1
  43. package/dist/config-hygiene.js +36 -13
  44. package/dist/coverage.d.ts +50 -0
  45. package/dist/coverage.d.ts.map +1 -0
  46. package/dist/coverage.js +92 -0
  47. package/dist/define.d.ts +8 -5
  48. package/dist/define.d.ts.map +1 -1
  49. package/dist/define.js +10 -6
  50. package/dist/distribution.d.ts +125 -0
  51. package/dist/distribution.d.ts.map +1 -0
  52. package/dist/distribution.js +227 -0
  53. package/dist/generated/index.d.ts +4234 -0
  54. package/dist/generated/index.d.ts.map +1 -0
  55. package/dist/generated/index.js +23 -0
  56. package/dist/index.d.ts +7 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +9 -1
  59. package/dist/init-templates.js +2 -2
  60. package/dist/integrity.json +11 -9
  61. package/dist/lint/audit-catalog.d.ts +3 -0
  62. package/dist/lint/audit-catalog.d.ts.map +1 -1
  63. package/dist/lint/audit-catalog.js +5 -0
  64. package/dist/lint/post-synth/index.d.ts.map +1 -1
  65. package/dist/lint/post-synth/index.js +4 -0
  66. package/dist/lint/post-synth/otel-helpers.d.ts +2 -1
  67. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -1
  68. package/dist/lint/post-synth/otel-helpers.js +1 -1
  69. package/dist/lint/post-synth/otel119.d.ts +1 -1
  70. package/dist/lint/post-synth/otel119.js +1 -1
  71. package/dist/lint/post-synth/otel125.d.ts +1 -1
  72. package/dist/lint/post-synth/otel125.js +1 -1
  73. package/dist/lint/post-synth/otel126.d.ts +1 -1
  74. package/dist/lint/post-synth/otel126.js +1 -1
  75. package/dist/lint/post-synth/otel127.d.ts +1 -1
  76. package/dist/lint/post-synth/otel127.js +1 -1
  77. package/dist/lint/post-synth/otel128.d.ts +8 -0
  78. package/dist/lint/post-synth/otel128.d.ts.map +1 -0
  79. package/dist/lint/post-synth/otel128.js +17 -0
  80. package/dist/lint/post-synth/otel129.d.ts +15 -0
  81. package/dist/lint/post-synth/otel129.d.ts.map +1 -0
  82. package/dist/lint/post-synth/otel129.js +92 -0
  83. package/dist/lsp/completions.d.ts +10 -1
  84. package/dist/lsp/completions.d.ts.map +1 -1
  85. package/dist/lsp/completions.js +29 -1
  86. package/dist/lsp/hover.d.ts +5 -1
  87. package/dist/lsp/hover.d.ts.map +1 -1
  88. package/dist/lsp/hover.js +16 -3
  89. package/dist/lsp/properties.d.ts +34 -0
  90. package/dist/lsp/properties.d.ts.map +1 -0
  91. package/dist/lsp/properties.js +135 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/meta.json +5 -0
  94. package/dist/metric-names.d.ts +36 -8
  95. package/dist/metric-names.d.ts.map +1 -1
  96. package/dist/metric-names.js +42 -9
  97. package/dist/model.d.ts +1 -1
  98. package/dist/model.js +1 -1
  99. package/dist/okf/index.md +3 -0
  100. package/dist/okf/rules/OTEL128.md +11 -0
  101. package/dist/okf/rules/OTEL129.md +11 -0
  102. package/dist/okf/types/ClickHouseExporter.md +9 -0
  103. package/dist/op/activities/collector-audit.d.ts +18 -6
  104. package/dist/op/activities/collector-audit.d.ts.map +1 -1
  105. package/dist/op/activities/collector-audit.js +37 -21
  106. package/dist/plugin.d.ts.map +1 -1
  107. package/dist/plugin.js +51 -8
  108. package/dist/rules/otel-helpers.ts +1 -1
  109. package/dist/rules/otel119.ts +1 -1
  110. package/dist/rules/otel125.ts +1 -1
  111. package/dist/rules/otel126.ts +1 -1
  112. package/dist/rules/otel127.ts +1 -1
  113. package/dist/rules/otel128.ts +22 -0
  114. package/dist/rules/otel129.ts +99 -0
  115. package/dist/serializer.d.ts +3 -0
  116. package/dist/serializer.d.ts.map +1 -1
  117. package/dist/serializer.js +12 -2
  118. package/dist/spec/builtins.d.ts +17 -0
  119. package/dist/spec/builtins.d.ts.map +1 -0
  120. package/dist/spec/builtins.js +45 -0
  121. package/dist/spec/fetch.d.ts +120 -0
  122. package/dist/spec/fetch.d.ts.map +1 -0
  123. package/dist/spec/fetch.js +282 -0
  124. package/dist/spec/parse.d.ts +77 -0
  125. package/dist/spec/parse.d.ts.map +1 -0
  126. package/dist/spec/parse.js +337 -0
  127. package/dist/spec/pin.d.ts +82 -0
  128. package/dist/spec/pin.d.ts.map +1 -0
  129. package/dist/spec/pin.js +126 -0
  130. package/dist/spec/snapshot-cli.d.ts +13 -0
  131. package/dist/spec/snapshot-cli.d.ts.map +1 -0
  132. package/dist/spec/snapshot-cli.js +31 -0
  133. package/dist/spec/snapshot.d.ts +9 -0
  134. package/dist/spec/snapshot.d.ts.map +1 -0
  135. package/dist/spec/snapshot.js +14 -0
  136. package/dist/types/index.d.ts +3 -2
  137. package/dist/validate-config.d.ts +1 -1
  138. package/dist/validate-config.d.ts.map +1 -1
  139. package/dist/validate.d.ts.map +1 -1
  140. package/dist/validate.js +1 -0
  141. package/dist/workspace-links.d.ts +25 -0
  142. package/dist/workspace-links.d.ts.map +1 -0
  143. package/dist/workspace-links.js +49 -0
  144. package/package.json +3 -2
  145. package/src/build/capability-plugin.ts +30 -0
  146. package/src/build/ocb-build.ts +73 -0
  147. package/src/clickhouse-exporter.e2e.test.ts +208 -0
  148. package/src/clickhouse-tables.test.ts +216 -0
  149. package/src/clickhouse-tables.ts +223 -0
  150. package/src/codegen/docs.ts +4 -2
  151. package/src/codegen/generate.ts +95 -19
  152. package/src/components/common.ts +4 -2
  153. package/src/components/connectors.ts +30 -77
  154. package/src/components/exporters.ts +128 -61
  155. package/src/components/extensions.ts +22 -36
  156. package/src/components/filtering.ts +17 -58
  157. package/src/components/k8s-receivers.test.ts +1 -1
  158. package/src/components/k8s-receivers.ts +3 -45
  159. package/src/components/overlay.ts +24 -0
  160. package/src/components/processors.ts +18 -63
  161. package/src/components/receivers.ts +20 -60
  162. package/src/components/sampling.ts +24 -37
  163. package/src/config-hygiene.ts +36 -13
  164. package/src/coverage.test.ts +37 -0
  165. package/src/coverage.ts +121 -0
  166. package/src/define.test.ts +2 -0
  167. package/src/define.ts +10 -6
  168. package/src/distribution.test.ts +250 -0
  169. package/src/distribution.ts +305 -0
  170. package/src/generated/index.ts +4576 -0
  171. package/src/generated/lexicon-otel.json +5 -0
  172. package/src/generated/properties.json +1 -0
  173. package/src/import/roundtrip.test.ts +16 -0
  174. package/src/import/testdata/clickhouse.yaml +37 -0
  175. package/src/import/testdata/fixtures.ts +17 -2
  176. package/src/import/testdata/upstream/clickhouse-exporter.yaml +60 -0
  177. package/src/index.ts +69 -0
  178. package/src/init-templates.ts +2 -2
  179. package/src/lint/audit-catalog.ts +15 -0
  180. package/src/lint/post-synth/index.ts +4 -0
  181. package/src/lint/post-synth/otel-helpers.ts +1 -1
  182. package/src/lint/post-synth/otel119.ts +1 -1
  183. package/src/lint/post-synth/otel125.ts +1 -1
  184. package/src/lint/post-synth/otel126.ts +1 -1
  185. package/src/lint/post-synth/otel127.ts +1 -1
  186. package/src/lint/post-synth/otel128.ts +22 -0
  187. package/src/lint/post-synth/otel129.ts +99 -0
  188. package/src/lsp/completions.ts +30 -1
  189. package/src/lsp/hover.ts +14 -3
  190. package/src/lsp/properties.test.ts +72 -0
  191. package/src/lsp/properties.ts +141 -0
  192. package/src/metric-names.test.ts +11 -0
  193. package/src/metric-names.ts +64 -16
  194. package/src/model.ts +1 -1
  195. package/src/op/activities/collector-audit.test.ts +29 -22
  196. package/src/op/activities/collector-audit.ts +38 -18
  197. package/src/plugin.test.ts +2 -0
  198. package/src/plugin.ts +57 -6
  199. package/src/serializer.ts +11 -2
  200. package/src/spec/builtins.ts +56 -0
  201. package/src/spec/fetch.ts +332 -0
  202. package/src/spec/otel-schemas.snapshot.json +13932 -0
  203. package/src/spec/parse.ts +364 -0
  204. package/src/spec/pin.ts +155 -0
  205. package/src/spec/snapshot-cli.ts +32 -0
  206. package/src/spec/snapshot.ts +19 -0
  207. package/src/spec/spec.test.ts +144 -0
  208. package/src/validate-config.ts +4 -3
  209. package/src/validate.ts +1 -0
  210. package/src/workspace-links.e2e.test.ts +91 -0
  211. package/src/workspace-links.ts +50 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Property-level completion and hover for the built-in components (#3361),
3
+ * from the descriptions in upstream's `config.schema.yaml`.
4
+ *
5
+ * Generation writes each built-in's property tree to
6
+ * `src/generated/properties.json`. Inside `new OtlpReceiver({ … })` the
7
+ * cursor's position is resolved to a path of keys (`protocols.grpc`), and the
8
+ * properties at that path are offered, with upstream's description and a short
9
+ * type. A word under the cursor at such a position hovers with the same text.
10
+ */
11
+
12
+ import { existsSync, readFileSync } from "fs";
13
+ import { dirname, join } from "path";
14
+ import { moduleSourceDir } from "@intentius/chant/module-source";
15
+ import type { PropertyDoc } from "../spec/parse";
16
+
17
+ type PropertyTrees = Record<string, Record<string, PropertyDoc>>;
18
+
19
+ let cached: PropertyTrees | undefined;
20
+
21
+ /** The generated property trees, or an empty set before `generate` has run. */
22
+ export function propertyTrees(): PropertyTrees {
23
+ if (cached) return cached;
24
+ const file = join(dirname(moduleSourceDir(import.meta.url)), "generated", "properties.json");
25
+ cached = existsSync(file) ? (JSON.parse(readFileSync(file, "utf-8")) as PropertyTrees) : {};
26
+ return cached;
27
+ }
28
+
29
+ /** Test seam: replace the loaded trees. */
30
+ export function setPropertyTrees(trees: PropertyTrees | undefined): void {
31
+ cached = trees;
32
+ }
33
+
34
+ /** Where the cursor is inside a constructor's config object. */
35
+ export interface PropertyPosition {
36
+ className: string;
37
+ /** The keys of the objects enclosing the cursor, outermost first. */
38
+ path: string[];
39
+ }
40
+
41
+ /**
42
+ * Resolve the text before the cursor to the built-in whose constructor
43
+ * argument holds it and the keys of the objects around it. Undefined when the
44
+ * cursor is not inside `new X({ … })`.
45
+ */
46
+ export function propertyPosition(before: string): PropertyPosition | undefined {
47
+ const re = /\bnew\s+(\w+)\s*\(\s*\{/g;
48
+ let match: RegExpExecArray | null;
49
+ let last: { className: string; start: number } | undefined;
50
+ while ((match = re.exec(before)) !== null) last = { className: match[1], start: match.index + match[0].length };
51
+ if (!last) return undefined;
52
+
53
+ // Walk from the constructor's `{`: each `{` opens the object of the key
54
+ // written just before it; `[` opens a list whose objects share that key.
55
+ const stack: Array<string | null> = [];
56
+ let pendingKey: string | null = null;
57
+ let i = last.start;
58
+ const text = before;
59
+ while (i < text.length) {
60
+ const ch = text[i];
61
+ if (ch === '"' || ch === "'" || ch === "`") {
62
+ const end = skipString(text, i);
63
+ const literal = text.slice(i + 1, end - 1);
64
+ // A quoted key: `"k8s.pod.name": …`
65
+ if (/^\s*:/.test(text.slice(end))) pendingKey = literal;
66
+ i = end;
67
+ continue;
68
+ }
69
+ if (ch === "/" && text[i + 1] === "/") {
70
+ const nl = text.indexOf("\n", i);
71
+ i = nl < 0 ? text.length : nl;
72
+ continue;
73
+ }
74
+ if (ch === "/" && text[i + 1] === "*") {
75
+ const close = text.indexOf("*/", i + 2);
76
+ i = close < 0 ? text.length : close + 2;
77
+ continue;
78
+ }
79
+ const ident = /^[A-Za-z_$][\w$]*/.exec(text.slice(i));
80
+ if (ident) {
81
+ if (/^\s*:/.test(text.slice(i + ident[0].length))) pendingKey = ident[0];
82
+ i += ident[0].length;
83
+ continue;
84
+ }
85
+ if (ch === "{" || ch === "[") {
86
+ // A list keeps its key, so its objects resolve to the list's items.
87
+ stack.push(ch === "[" ? `[${pendingKey ?? ""}` : pendingKey);
88
+ pendingKey = null;
89
+ } else if (ch === "}" || ch === "]") {
90
+ if (stack.length === 0) return undefined; // the constructor's object closed
91
+ stack.pop();
92
+ } else if (ch === "," || ch === "(" || ch === ")") {
93
+ pendingKey = null;
94
+ }
95
+ i++;
96
+ }
97
+
98
+ const path: string[] = [];
99
+ let inList: string | undefined;
100
+ for (const entry of stack) {
101
+ if (entry === null) {
102
+ if (inList === undefined) return undefined; // an object whose key is unknown
103
+ inList = undefined; // an item of the list just entered
104
+ continue;
105
+ }
106
+ if (entry.startsWith("[")) {
107
+ inList = entry.slice(1);
108
+ if (inList) path.push(inList);
109
+ continue;
110
+ }
111
+ path.push(entry);
112
+ inList = undefined;
113
+ }
114
+ if (inList !== undefined) return undefined; // directly inside a list, not an item
115
+ return { className: last.className, path };
116
+ }
117
+
118
+ function skipString(text: string, start: number): number {
119
+ const quote = text[start];
120
+ let i = start + 1;
121
+ while (i < text.length && text[i] !== quote) i += text[i] === "\\" ? 2 : 1;
122
+ return Math.min(i + 1, text.length);
123
+ }
124
+
125
+ /** The properties at a path of a built-in's tree, or undefined when the path leaves it. */
126
+ export function propertiesAt(position: PropertyPosition): Record<string, PropertyDoc> | undefined {
127
+ let level: Record<string, PropertyDoc> | undefined = propertyTrees()[position.className];
128
+ for (const key of position.path) {
129
+ level = level?.[key]?.properties;
130
+ if (!level) return undefined;
131
+ }
132
+ return level;
133
+ }
134
+
135
+ /** Markdown for one property: its type, deprecation and upstream description. */
136
+ export function propertyMarkdown(name: string, doc: PropertyDoc, className: string): string {
137
+ const lines = [`**${name}**: \`${doc.type}\`${doc.deprecated ? " (deprecated)" : ""}`];
138
+ if (doc.description) lines.push("", doc.description);
139
+ lines.push("", `\`${className}\` config, from upstream config.schema.yaml.`);
140
+ return lines.join("\n");
141
+ }
@@ -57,6 +57,17 @@ describe("spanMetricsNames", () => {
57
57
  expect(spanMetricsNames(connector, { add_metric_suffixes: false }).duration?.prometheus).toBe("spans_duration");
58
58
  });
59
59
 
60
+ test("the prometheus exporter's translation_strategy decides suffixes; it ignores add_metric_suffixes at the pin", () => {
61
+ const connector = new SpanMetricsConnector({ namespace: "spans", histogram: { unit: "s" } });
62
+ const without = new PrometheusExporter({ endpoint: "0.0.0.0:8889", translation_strategy: "UnderscoreEscapingWithoutSuffixes" });
63
+ expect(spanMetricsNames(connector, without).duration?.prometheus).toBe("spans_duration");
64
+ const ignored = new PrometheusExporter({ endpoint: "0.0.0.0:8889", add_metric_suffixes: false });
65
+ expect(spanMetricsNames(connector, ignored).duration?.prometheus).toBe("spans_duration_seconds");
66
+ const utf8 = new PrometheusExporter({ endpoint: "0.0.0.0:8889", translation_strategy: "NoUTF8EscapingWithSuffixes" });
67
+ expect(spanMetricsNames(connector, utf8).calls.prometheus).toBe("spans.calls_total");
68
+ expect(spanMetricsNames(connector, { translation_strategy: "NoTranslation" }).calls.prometheus).toBe("spans.calls");
69
+ });
70
+
60
71
  test("only a spanmetrics connector", () => {
61
72
  expect(() => spanMetricsNames(new PrometheusExporter({ endpoint: "0.0.0.0:8889" }) as never)).toThrow(/spanmetrics/);
62
73
  });
@@ -11,9 +11,16 @@
11
11
  * with the exporter's defaults: `.` and any other character Prometheus does
12
12
  * not allow become `_`; counters take `_total`; a unit becomes a suffix
13
13
  * (`ms` is `_milliseconds`, `s` is `_seconds`); the exporter's `namespace`
14
- * is a prefix joined with `_`; `add_metric_suffixes: false` drops the
15
- * suffixes. Attribute names become label names the same way
16
- * (`service.name` is `service_name`).
14
+ * is a prefix joined with `_`. The exporter's `translation_strategy` decides
15
+ * the escaping and the suffixes: the default, `UnderscoreEscapingWithSuffixes`,
16
+ * is what is described here; `UnderscoreEscapingWithoutSuffixes` drops the
17
+ * suffixes; `NoUTF8EscapingWithSuffixes` keeps the name's characters;
18
+ * `NoTranslation` does neither. From collector-contrib v0.154.0 the
19
+ * `prometheus` exporter ignores the deprecated `add_metric_suffixes`, so a
20
+ * name read from that exporter follows `translation_strategy` alone; the
21
+ * `prometheusremotewrite` exporter still honours it when no strategy is set.
22
+ * Attribute names become label names the same way (`service.name` is
23
+ * `service_name`).
17
24
  *
18
25
  * This module holds types and string functions only, so a lexicon that
19
26
  * builds queries from it does not load the collector components.
@@ -52,14 +59,54 @@ const UNIT_WORDS: Record<string, string> = {
52
59
  "1": "ratio",
53
60
  };
54
61
 
62
+ /** The `translation_strategy` values the Prometheus exporters take. */
63
+ export type PrometheusTranslationStrategy =
64
+ | "UnderscoreEscapingWithSuffixes"
65
+ | "UnderscoreEscapingWithoutSuffixes"
66
+ | "NoUTF8EscapingWithSuffixes"
67
+ | "NoTranslation";
68
+
55
69
  /** How the `prometheus` exporter is configured, as far as naming goes. */
56
70
  export interface PrometheusNaming {
57
71
  /** The exporter's `namespace`, put in front of every name and joined with `_`. */
58
72
  namespace?: string;
59
- /** The exporter's `add_metric_suffixes` (default true): unit and `_total` suffixes. */
73
+ /** The exporter's `translation_strategy`. Default `UnderscoreEscapingWithSuffixes`. */
74
+ translation_strategy?: PrometheusTranslationStrategy | (string & {});
75
+ /**
76
+ * `false` drops the unit and `_total` suffixes when no `translation_strategy`
77
+ * is set, as the `prometheusremotewrite` exporter reads it. The `prometheus`
78
+ * exporter ignores it at the pinned release, and {@link exporterNaming}
79
+ * leaves it out for that exporter.
80
+ *
81
+ * @deprecated Upstream deprecates it for `translation_strategy`.
82
+ */
60
83
  add_metric_suffixes?: boolean;
61
84
  }
62
85
 
86
+ /**
87
+ * The naming a declared `prometheus` exporter (or its config) applies: its
88
+ * `namespace` and `translation_strategy`. `add_metric_suffixes` is left out,
89
+ * since the exporter ignores it at the pinned release. A plain
90
+ * `PrometheusNaming` is returned as given.
91
+ */
92
+ export function exporterNaming(exporter: PrometheusNaming | { props?: PrometheusNaming; componentType?: unknown } | undefined): PrometheusNaming {
93
+ if (!exporter) return {};
94
+ if ("props" in exporter && typeof exporter.props === "object" && exporter.props !== null) {
95
+ const { add_metric_suffixes: _ignored, ...rest } = exporter.props;
96
+ return { ...(rest.namespace !== undefined ? { namespace: rest.namespace } : {}), ...(rest.translation_strategy !== undefined ? { translation_strategy: rest.translation_strategy } : {}) };
97
+ }
98
+ return exporter as PrometheusNaming;
99
+ }
100
+
101
+ /** Whether a naming escapes names to `[A-Za-z0-9_:]` and whether it adds suffixes. */
102
+ function strategyOf(naming: PrometheusNaming): { escape: boolean; suffixes: boolean } {
103
+ const strategy = naming.translation_strategy ?? (naming.add_metric_suffixes === false ? "UnderscoreEscapingWithoutSuffixes" : "UnderscoreEscapingWithSuffixes");
104
+ return {
105
+ escape: strategy.startsWith("UnderscoreEscaping"),
106
+ suffixes: strategy !== "UnderscoreEscapingWithoutSuffixes" && strategy !== "NoTranslation",
107
+ };
108
+ }
109
+
63
110
  /** An attribute name as a Prometheus label: `service.name` is `service_name`. */
64
111
  export function prometheusLabel(attribute: string): string {
65
112
  const label = attribute.replace(/[^A-Za-z0-9_]/g, "_");
@@ -73,9 +120,11 @@ export function prometheusLabel(attribute: string): string {
73
120
  * @param unit The metric's unit, e.g. `ms`. Units in braces (`{call}`) add nothing.
74
121
  */
75
122
  export function prometheusMetricName(name: string, type: "sum" | "histogram", unit?: string, naming: PrometheusNaming = {}): string {
76
- let out = name.replace(/[^A-Za-z0-9_:]/g, "_");
77
- if (naming.namespace) out = `${naming.namespace.replace(/[^A-Za-z0-9_:]/g, "_")}_${out}`;
78
- if (naming.add_metric_suffixes === false) return out;
123
+ const { escape, suffixes } = strategyOf(naming);
124
+ const clean = (s: string) => (escape ? s.replace(/[^A-Za-z0-9_:]/g, "_") : s);
125
+ let out = clean(name);
126
+ if (naming.namespace) out = `${clean(naming.namespace)}_${out}`;
127
+ if (!suffixes) return out;
79
128
  const word = unit && !unit.startsWith("{") ? (UNIT_WORDS[unit] ?? unit.replace(/[^A-Za-z0-9_:]/g, "_")) : undefined;
80
129
  if (word && !out.endsWith(`_${word}`)) out = `${out}_${word}`;
81
130
  if (type === "sum" && !out.endsWith("_total")) out = `${out}_total`;
@@ -85,11 +134,11 @@ export function prometheusMetricName(name: string, type: "sum" | "histogram", un
85
134
  /** The parts of a spanmetrics config the metric names depend on. */
86
135
  export interface SpanMetricsNamingConfig {
87
136
  namespace?: string;
88
- dimensions?: Array<{ name: string }>;
89
- calls_dimensions?: Array<{ name: string }>;
137
+ dimensions?: Array<{ name?: string }>;
138
+ calls_dimensions?: Array<{ name?: string }>;
90
139
  exclude_dimensions?: string[];
91
- histogram?: { disable?: boolean; unit?: "ms" | "s"; dimensions?: Array<{ name: string }> };
92
- events?: { enabled?: boolean; dimensions?: Array<{ name: string }> };
140
+ histogram?: { disable?: boolean; unit?: "ms" | "s"; dimensions?: Array<{ name?: string }> };
141
+ events?: { enabled?: boolean; dimensions?: Array<{ name?: string }> };
93
142
  }
94
143
 
95
144
  /** The metrics one `spanmetrics` connector emits, and the labels a query selects them by. */
@@ -139,13 +188,13 @@ export function spanMetricsNames(
139
188
  exporter?: PrometheusNaming | { props?: PrometheusNaming },
140
189
  ): SpanMetricsNames {
141
190
  const c = configOf(connector);
142
- const naming: PrometheusNaming =
143
- exporter && "props" in exporter && typeof exporter.props === "object" ? (exporter.props as PrometheusNaming) : ((exporter ?? {}) as PrometheusNaming);
191
+ const naming = exporterNaming(exporter);
144
192
  const namespace = c.namespace ?? SPANMETRICS_DEFAULT_NAMESPACE;
145
193
  const metricName = (n: string) => (namespace === "" ? n : `${namespace}.${n}`);
146
194
  const excluded = new Set(c.exclude_dimensions ?? []);
147
195
  const defaults = SPANMETRICS_DEFAULT_DIMENSIONS.filter((d) => !excluded.has(d));
148
- const names = (extra?: Array<{ name: string }>) => (extra ?? []).map((d) => d.name);
196
+ // A `glob` dimension names no single attribute, so it adds no fixed label.
197
+ const names = (extra?: Array<{ name?: string }>) => (extra ?? []).flatMap((d) => (d.name === undefined ? [] : [d.name]));
149
198
  const common = [...defaults, ...names(c.dimensions)];
150
199
 
151
200
  const metric = (n: string, type: "sum" | "histogram", dims: string[], unit?: string): CollectorMetric => ({
@@ -215,8 +264,7 @@ export function serviceGraphNames(
215
264
  throw new Error(`serviceGraphNames: expected a servicegraph connector, got ${d.componentType}`);
216
265
  }
217
266
  const c = (typeof d.componentType === "string" ? (d.props ?? {}) : connector) as ServiceGraphNamingConfig;
218
- const naming: PrometheusNaming =
219
- exporter && "props" in exporter && typeof exporter.props === "object" ? (exporter.props as PrometheusNaming) : ((exporter ?? {}) as PrometheusNaming);
267
+ const naming = exporterNaming(exporter);
220
268
  const dimensions = [
221
269
  "client",
222
270
  "server",
package/src/model.ts CHANGED
@@ -125,7 +125,7 @@ export interface ComponentTypeAlias {
125
125
 
126
126
  /**
127
127
  * The renamed built-ins. chant emits `builtin`, which the pinned collector
128
- * (`COLLECTOR_PIN`, v0.130.0) knows and newer ones still accept. A config
128
+ * (`COLLECTOR_PIN`) still accepts as each component's deprecated type. A config
129
129
  * written for a newer collector may use `type`, so code that reads a config
130
130
  * (the importer, the checks, the k8s composites) accepts both.
131
131
  */
@@ -61,17 +61,19 @@ describe("collectorAudit's pieces", () => {
61
61
  ]);
62
62
  });
63
63
 
64
- test("bumpPins moves only the two pin versions in define.ts", () => {
65
- const text = readFileSync(join(import.meta.dirname, "..", "..", "define.ts"), "utf8");
66
- const out = bumpPins(text, { collector: "v0.140.0", genai: "v1.50.0" });
64
+ test("bumpPins moves only the two pin versions, in spec/pin.ts and define.ts", () => {
65
+ const define = readFileSync(join(import.meta.dirname, "..", "..", "define.ts"), "utf8");
66
+ const pin = readFileSync(join(import.meta.dirname, "..", "..", "spec", "pin.ts"), "utf8");
67
+ const out = bumpPins({ define, pin }, { collector: "v0.170.0", genai: "v1.50.0" });
67
68
  const block = (text: string, name: string) => new RegExp(`export const ${name}: SchemaPin = Object\\.freeze\\(\\{[^}]*\\}`).exec(text)![0];
68
- expect(block(out, "COLLECTOR_PIN")).toContain('version: "v0.140.0"');
69
- expect(block(out, "GENAI_SEMCONV_PIN")).toContain('version: "v1.50.0"');
70
- // SEMCONV_PIN, and the custom-component pin in define.ts's doc comment, are left alone.
71
- expect(block(out, "SEMCONV_PIN")).toBe(block(text, "SEMCONV_PIN"));
72
- const changed = out.split("\n").filter((l, i) => l !== text.split("\n")[i]);
73
- expect(changed).toHaveLength(2);
74
- expect(() => bumpPins("nothing", { collector: "v1.0.0" })).toThrow(/no COLLECTOR_PIN/);
69
+ expect(out.pin).toContain('export const OTELCOL_CONTRIB_VERSION = "v0.170.0";');
70
+ expect(block(out.define, "GENAI_SEMCONV_PIN")).toContain('version: "v1.50.0"');
71
+ // SEMCONV_PIN, the k8s distribution pin and the custom-component pin in define.ts's doc comment are left alone.
72
+ expect(block(out.define, "SEMCONV_PIN")).toBe(block(define, "SEMCONV_PIN"));
73
+ const changed = (a: string, b: string) => a.split("\n").filter((l, i) => l !== b.split("\n")[i]);
74
+ expect(changed(out.define, define)).toHaveLength(1);
75
+ expect(changed(out.pin, pin)).toHaveLength(1);
76
+ expect(() => bumpPins({ define: "nothing", pin: "nothing" }, { collector: "v1.0.0" })).toThrow(/no OTELCOL_CONTRIB_VERSION/);
75
77
  });
76
78
 
77
79
  test("semver ordering", () => {
@@ -80,6 +82,11 @@ describe("collectorAudit's pieces", () => {
80
82
  });
81
83
  });
82
84
 
85
+ /** The releases after the pin, whatever it is. */
86
+ const minorAfter = (v: string, n: number) => v.replace(/^v(\d+)\.(\d+)\.\d+$/, (_, major: string, minor: string) => `v${major}.${Number(minor) + n}.0`);
87
+ const NEXT = minorAfter(COLLECTOR_PIN.version, 1);
88
+ const AFTER_NEXT = minorAfter(COLLECTOR_PIN.version, 2);
89
+
83
90
  describe("collectorAudit", () => {
84
91
  test("current pins and no stability reading: no findings, report mode does nothing else", async () => {
85
92
  const { f } = fakeFetch([COLLECTOR_PIN.version, "v0.129.0"], [GENAI_SEMCONV_PIN.version]);
@@ -90,30 +97,30 @@ describe("collectorAudit", () => {
90
97
  });
91
98
 
92
99
  test("pins behind the newest releases are findings, prereleases ignored", async () => {
93
- const { f } = fakeFetch(["v0.132.0", "v0.131.0", COLLECTOR_PIN.version], ["v1.42.0", GENAI_SEMCONV_PIN.version]);
100
+ const { f } = fakeFetch([AFTER_NEXT, NEXT, COLLECTOR_PIN.version], ["v1.42.0", GENAI_SEMCONV_PIN.version]);
94
101
  const r = await collectorAudit({ stability: false, _fetch: f });
95
102
  expect(r.findings.map((x) => x.kind)).toEqual(["collector-pin-behind", "semconv-pin-behind"]);
96
- expect(r.collector).toEqual({ pin: COLLECTOR_PIN.version, latest: "v0.132.0", behind: 2 });
103
+ expect(r.collector).toEqual({ pin: COLLECTOR_PIN.version, latest: AFTER_NEXT, behind: 2 });
97
104
  expect(r.semconv.behind).toBe(1);
98
105
  });
99
106
 
100
107
  test("component stability at the pin and at the newest release, contrib then core, within the budget", async () => {
101
108
  const meta = {
102
109
  // otlp is a core component: contrib answers 404 for it.
103
- [`opentelemetry-collector/v0.131.0/receiver/otlpreceiver`]: stability({ stable: ["traces"], deprecated: ["logs"] }),
110
+ [`opentelemetry-collector/${NEXT}/receiver/otlpreceiver`]: stability({ stable: ["traces"], deprecated: ["logs"] }),
104
111
  [`opentelemetry-collector/${COLLECTOR_PIN.version}/receiver/otlpreceiver`]: stability({ stable: ["traces", "logs"] }),
105
112
  };
106
- const { f, urls } = fakeFetch(["v0.131.0", COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version], meta);
113
+ const { f, urls } = fakeFetch([NEXT, COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version], meta);
107
114
  const r = await collectorAudit({ _fetch: f, stabilityBudget: 500 });
108
- expect(r.findings).toContainEqual({ kind: "deprecated", subject: "receiver/otlp", detail: "logs deprecated at v0.131.0" });
115
+ expect(r.findings).toContainEqual({ kind: "deprecated", subject: "receiver/otlp", detail: `logs deprecated at ${NEXT}` });
109
116
  expect(r.unchecked).not.toContain("receiver/otlp");
110
117
  expect(r.unchecked.length).toBe(builtinComponents().length - 1);
111
118
  // Looked for in contrib first, then core.
112
- const otlp = urls.filter((u) => u.includes("/v0.131.0/receiver/otlpreceiver/"));
119
+ const otlp = urls.filter((u) => u.includes(`/${NEXT}/receiver/otlpreceiver/`));
113
120
  expect(otlp[0]).toContain("opentelemetry-collector-contrib");
114
121
  expect(otlp[1]).toContain("open-telemetry/opentelemetry-collector/");
115
122
 
116
- const capped = fakeFetch(["v0.131.0", COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version], meta);
123
+ const capped = fakeFetch([NEXT, COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version], meta);
117
124
  await collectorAudit({ _fetch: capped.f, stabilityBudget: 3 });
118
125
  expect(capped.urls.filter((u) => u.includes("raw.githubusercontent.com"))).toHaveLength(3);
119
126
  });
@@ -127,7 +134,7 @@ describe("collectorAudit", () => {
127
134
  });
128
135
 
129
136
  test("pull-request mode bumps the pins in a worktree and opens a pull request", async () => {
130
- const { f } = fakeFetch(["v0.131.0", COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version]);
137
+ const { f } = fakeFetch([NEXT, COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version]);
131
138
  const calls: string[] = [];
132
139
  const lexiconDir = join(import.meta.dirname, "..", "..", "..");
133
140
  const run: CommandRunner = async (bin, args) => {
@@ -143,7 +150,7 @@ describe("collectorAudit", () => {
143
150
  });
144
151
 
145
152
  test("pull-request mode end to end against a recorded git and gh", async () => {
146
- const { f } = fakeFetch(["v0.131.0", COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version]);
153
+ const { f } = fakeFetch([NEXT, COLLECTOR_PIN.version], [GENAI_SEMCONV_PIN.version]);
147
154
  const calls: string[] = [];
148
155
  const lexiconDir = join(import.meta.dirname, "..", "..", "..");
149
156
  const { mkdirSync } = await import("node:fs");
@@ -151,15 +158,15 @@ describe("collectorAudit", () => {
151
158
  calls.push(`${bin} ${args.join(" ")}`);
152
159
  if (args[0] === "rev-parse") return join(lexiconDir, "..", "..") + "\n";
153
160
  if (args[0] === "symbolic-ref") return "origin/main\n";
154
- if (args[0] === "worktree" && args[1] === "add") mkdirSync(join(args[5], "lexicons", "otel", "src"), { recursive: true });
161
+ if (args[0] === "worktree" && args[1] === "add") mkdirSync(join(args[5], "lexicons", "otel", "src", "spec"), { recursive: true });
155
162
  if (bin === "gh" && args[1] === "list") return "[]";
156
163
  if (bin === "gh" && args[1] === "create") return "https://github.com/INTENTIUS/chant/pull/1\n";
157
164
  return "";
158
165
  };
159
166
  const r = await collectorAudit({ mode: "pull-request", stability: false, lexiconDir, _fetch: f, _run: run });
160
167
  expect(r.prUrl).toBe("https://github.com/INTENTIUS/chant/pull/1");
161
- expect(calls).toContain("git add lexicons/otel/src/define.ts");
162
- expect(calls.some((c) => c.startsWith("git commit -m chore(otel): move COLLECTOR_PIN to v0.131.0"))).toBe(true);
168
+ expect(calls).toContain("git add lexicons/otel/src/spec/pin.ts");
169
+ expect(calls.some((c) => c.startsWith(`git commit -m chore(otel): move COLLECTOR_PIN to ${NEXT}`))).toBe(true);
163
170
  expect(calls.some((c) => c.startsWith("git push --force origin HEAD:refs/heads/chant/otel-pins"))).toBe(true);
164
171
  expect(calls.some((c) => c.startsWith("gh pr create --head chant/otel-pins --base main"))).toBe(true);
165
172
  });
@@ -16,10 +16,12 @@
16
16
  *
17
17
  * `mode` says what to do with the findings: `report` returns them;
18
18
  * `issue` keeps one open issue current; `pull-request` also bumps the pins
19
- * in the lexicon's `src/define.ts` on a proposal branch and opens (or
19
+ * (`OTELCOL_CONTRIB_VERSION` in `src/spec/pin.ts` for `COLLECTOR_PIN`, and
20
+ * `GENAI_SEMCONV_PIN` in `src/define.ts`) on a proposal branch and opens (or
20
21
  * edits) a pull request with the findings as its body. Only the pin values
21
- * change: the config types written against the old pin are the reviewer's
22
- * to move, which the body says.
22
+ * change: refreshing the schema snapshot the config types are generated from
23
+ * (`just snapshot`, #3361) is the reviewer's, which the body says, and
24
+ * generation refuses until it is done.
23
25
  */
24
26
 
25
27
  import { mkdtempSync, readFileSync } from "node:fs";
@@ -164,17 +166,27 @@ function behindCount(releases: Release[], pin: string): number {
164
166
  return releases.filter((r) => compareSemver(r.tag, pin) > 0).length;
165
167
  }
166
168
 
167
- /** `src/define.ts` with `COLLECTOR_PIN` and `GENAI_SEMCONV_PIN` moved to the given versions. */
168
- export function bumpPins(text: string, to: { collector?: string; genai?: string }): string {
169
- let out = text;
170
- const bump = (name: string, version: string) => {
171
- const re = new RegExp(`(export const ${name}: SchemaPin = Object\\.freeze\\(\\{[^}]*?version:\\s*")([^"]+)(")`);
172
- if (!re.test(out)) throw new Error(`define.ts has no ${name} with a version to bump`);
173
- out = out.replace(re, `$1${version}$3`);
174
- };
175
- if (to.collector) bump("COLLECTOR_PIN", to.collector);
176
- if (to.genai) bump("GENAI_SEMCONV_PIN", to.genai);
177
- return out;
169
+ /**
170
+ * The pin files with `COLLECTOR_PIN` and `GENAI_SEMCONV_PIN` moved to the
171
+ * given versions: `COLLECTOR_PIN` follows `OTELCOL_CONTRIB_VERSION` in
172
+ * `src/spec/pin.ts`, and `GENAI_SEMCONV_PIN` is in `src/define.ts`.
173
+ */
174
+ export function bumpPins(
175
+ texts: { define: string; pin: string },
176
+ to: { collector?: string; genai?: string },
177
+ ): { define: string; pin: string } {
178
+ let { define, pin } = texts;
179
+ if (to.collector) {
180
+ const re = /(export const OTELCOL_CONTRIB_VERSION = ")([^"]+)(")/;
181
+ if (!re.test(pin)) throw new Error("spec/pin.ts has no OTELCOL_CONTRIB_VERSION (COLLECTOR_PIN) to bump");
182
+ pin = pin.replace(re, `$1${to.collector}$3`);
183
+ }
184
+ if (to.genai) {
185
+ const re = /(export const GENAI_SEMCONV_PIN: SchemaPin = Object\.freeze\(\{[^}]*?version:\s*")([^"]+)(")/;
186
+ if (!re.test(define)) throw new Error("define.ts has no GENAI_SEMCONV_PIN with a version to bump");
187
+ define = define.replace(re, `$1${to.genai}$3`);
188
+ }
189
+ return { define, pin };
178
190
  }
179
191
 
180
192
  export function renderCollectorAuditSummary(r: Omit<CollectorAuditResult, "summary" | "mode">): string {
@@ -248,16 +260,24 @@ export async function collectorAudit(args: CollectorAuditArgs = {}): Promise<Col
248
260
  const lexiconDir = resolve(args.lexiconDir ?? join(cwd, "lexicons", "otel"));
249
261
  const root = (await run("git", ["rev-parse", "--show-toplevel"], lexiconDir)).trim();
250
262
  const definePath = join(lexiconDir, "src", "define.ts");
251
- const content = bumpPins(readFileSync(definePath, "utf8"), to);
263
+ const pinPath = join(lexiconDir, "src", "spec", "pin.ts");
264
+ const bumped = bumpPins({ define: readFileSync(definePath, "utf8"), pin: readFileSync(pinPath, "utf8") }, to);
265
+ const files = [
266
+ ...(to.collector ? [{ path: relative(root, pinPath), content: bumped.pin }] : []),
267
+ ...(to.genai ? [{ path: relative(root, definePath), content: bumped.define }] : []),
268
+ ];
252
269
  const moved = [to.collector ? `COLLECTOR_PIN to ${to.collector}` : "", to.genai ? `GENAI_SEMCONV_PIN to ${to.genai}` : ""].filter(Boolean).join(" and ");
253
270
  result.prUrl = await proposePullRequest(run, {
254
271
  root,
255
272
  branch: args.branch ?? "chant/otel-pins",
256
273
  title: `chore(otel): move ${moved}`,
257
274
  body:
258
- `${summary}\n\nThis moves the pin values only. The config types, defaults and checks written against ${collector.pin} ` +
259
- "still need reading against the release notes before this merges.\n",
260
- files: [{ path: relative(root, definePath), content }],
275
+ `${summary}\n\nThis moves the pin values only. ` +
276
+ (to.collector
277
+ ? "Run `just snapshot` in lexicons/otel, paste the printed OTEL_SPEC_PIN and regenerate: the config types are generated from the snapshot, and generation refuses a snapshot taken at another version. "
278
+ : "") +
279
+ `The defaults and checks written against ${collector.pin} still need reading against the release notes before this merges.\n`,
280
+ files,
261
281
  commitMessage: `chore(otel): move ${moved}`,
262
282
  worktreeDir: join(mkdtempSync(join(tmpdir(), "chant-otel-pins-")), "wt"),
263
283
  });
@@ -44,6 +44,8 @@ describe("otel plugin", () => {
44
44
  "OTEL125",
45
45
  "OTEL126",
46
46
  "OTEL127",
47
+ "OTEL128",
48
+ "OTEL129",
47
49
  ]);
48
50
  for (const id of ids) expect(id.startsWith("OTEL")).toBe(true);
49
51
  });
package/src/plugin.ts CHANGED
@@ -18,6 +18,19 @@ import { compositeCatalog } from "./composites/catalog";
18
18
  import { DEFAULT_TEMPLATE, GENAI_TEMPLATE, K8S_AGENT_TEMPLATE } from "./init-templates";
19
19
  import { COLLECTOR_PIN } from "./define";
20
20
  import { collectorTopologyOf } from "./topology";
21
+ import { otelWorkspaceLinks } from "./workspace-links";
22
+
23
+ /** opentelemetry-collector-releases also tags its tools (`cmd/builder/v0.163.2`); only the plain release tags are distributions. */
24
+ function releaseTag(tag: string): string | null {
25
+ return /^v\d+\.\d+\.\d+$/.test(tag) ? tag : null;
26
+ }
27
+
28
+ const SNAPSHOT_MOVES =
29
+ "The schema snapshot and its digest move with the version: set the version in src/spec/pin.ts, run `just snapshot` " +
30
+ "(lexicons/otel) on a networked machine, paste the printed OTEL_SPEC_PIN into src/spec/pin.ts, run `npm run generate`, " +
31
+ "and read the diffs of src/spec/otel-schemas.snapshot.json and the generated types. Moving otelcol-contrib also moves " +
32
+ "COLLECTOR_PIN and COLLECTOR_IMAGE, the default image of the k8s, docker and fly collector composites, so it is a " +
33
+ "level-0 change that needs an entry on the exceptions list (#2525).";
21
34
 
22
35
  const catalogResource: McpResourceContribution = {
23
36
  uri: "otel:resource-catalog",
@@ -54,15 +67,47 @@ export const otelPlugin: LexiconPlugin = {
54
67
  printValidationResult(await validate());
55
68
  },
56
69
 
57
- async coverage(_options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
58
- const components = BUILTIN_CATALOG.filter((c) => c.type !== undefined);
59
- const byKind = new Map<string, string[]>();
60
- for (const c of components) byKind.set(c.kind, [...(byKind.get(c.kind) ?? []), c.type!]);
61
- console.error(`otel: ${components.length} built-in components, typed against ${COLLECTOR_PIN.source} ${COLLECTOR_PIN.version}`);
62
- for (const [kind, types] of byKind) console.error(` ${kind}s: ${types.join(", ")}`);
70
+ async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
71
+ const { coverageReport, formatCoverage } = await import("./coverage");
72
+ const report = coverageReport();
73
+ console.error(formatCoverage(report));
74
+ if (options?.verbose) {
75
+ for (const d of report.distributions) {
76
+ console.error(` ${d.distribution}, reached through defineComponent: ${[...d.schemaNotBuiltin, ...d.noSchemaNotBuiltin].join(", ")}`);
77
+ }
78
+ }
63
79
  console.error(" anything else: defineComponent (see docs: custom components)");
64
80
  },
65
81
 
82
+ // #3361: coverage against the otelcol-contrib manifest, offline from the
83
+ // committed schema snapshot.
84
+ async coverageReport(): Promise<{ unaccountedKinds?: string[] }> {
85
+ const { coverageReport } = await import("./coverage");
86
+ return coverageReport();
87
+ },
88
+
89
+ // The distribution manifests the generated config types are scoped by
90
+ // (#3361): otelcol-contrib, which COLLECTOR_PIN and COLLECTOR_IMAGE follow,
91
+ // and otelcol-k8s for the coverage report.
92
+ upstreamPin: {
93
+ label: "otelcol-contrib",
94
+ file: "src/spec/pin.ts",
95
+ pattern: /export const OTELCOL_CONTRIB_VERSION = "([^"]+)"/,
96
+ replace: (v: string, line: string) => line.replace(/= "[^"]+"/, `= "${v}"`),
97
+ alsoMoves: SNAPSHOT_MOVES,
98
+ upstream: { owner: "open-telemetry", repo: "opentelemetry-collector-releases", kind: "releases", tagVersion: releaseTag },
99
+ },
100
+ upstreamPins: [
101
+ {
102
+ label: "otelcol-k8s",
103
+ file: "src/spec/pin.ts",
104
+ pattern: /export const OTELCOL_K8S_VERSION = "([^"]+)"/,
105
+ replace: (v: string, line: string) => line.replace(/= "[^"]+"/, `= "${v}"`),
106
+ alsoMoves: SNAPSHOT_MOVES,
107
+ upstream: { owner: "open-telemetry", repo: "opentelemetry-collector-releases", kind: "releases", tagVersion: releaseTag },
108
+ },
109
+ ],
110
+
66
111
  async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
67
112
  const { packageLexicon } = await import("./codegen/package");
68
113
  const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
@@ -88,6 +133,12 @@ export const otelPlugin: LexiconPlugin = {
88
133
  return { collector: topology };
89
134
  },
90
135
 
136
+ // The tables and columns a clickhouse exporter writes, as links to the sql
137
+ // member that declares them (#4397), for `chant workspace check`.
138
+ workspaceLinks(entities) {
139
+ return otelWorkspaceLinks(entities);
140
+ },
141
+
91
142
  postSynthChecks() {
92
143
  return postSynthChecks;
93
144
  },
package/src/serializer.ts CHANGED
@@ -7,6 +7,9 @@
7
7
  * the order they are declared in, and each component's config keeps the key
8
8
  * order it was written in, since collector docs and diffs read that way.
9
9
  *
10
+ * A project that declares a `Distribution` also gets the ocb builder manifest,
11
+ * as `builder-config.yaml` beside the config (see `distribution.ts`).
12
+ *
10
13
  * A collector config has no metadata channel, so there is no ownership marker
11
14
  * to stamp. A custom component's schema pin is written as a `# chant:` comment
12
15
  * line above the config (see `define.ts`).
@@ -17,6 +20,7 @@ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
17
20
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
18
21
  import { buildCollectorConfig } from "./collector";
19
22
  import { emitCollectorYaml } from "./yaml";
23
+ import { DISTRIBUTION_MANIFEST_FILE, distributionYaml, isDistributionEntity } from "./distribution";
20
24
 
21
25
  export const otelSerializer: Serializer = {
22
26
  name: "otel",
@@ -25,7 +29,12 @@ export const otelSerializer: Serializer = {
25
29
  serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string | SerializerResult {
26
30
  const built = buildCollectorConfig(entities);
27
31
  const yaml = emitCollectorYaml(built.config, { header: built.header });
28
- if (built.warnings.length === 0) return yaml;
29
- return { primary: yaml, warnings: built.warnings };
32
+ const warnings = [...built.warnings];
33
+ // A declared Distribution (#3374) adds the ocb builder manifest beside the config.
34
+ const distributions = [...entities.values()].filter(isDistributionEntity);
35
+ if (distributions.length > 1) warnings.push("otel: more than one Distribution is declared; the first one is written");
36
+ const files = distributions.length > 0 ? { [DISTRIBUTION_MANIFEST_FILE]: distributionYaml(distributions[0]) } : undefined;
37
+ if (warnings.length === 0 && !files) return yaml;
38
+ return { primary: yaml, ...(files ? { files } : {}), ...(warnings.length > 0 ? { warnings } : {}) };
30
39
  },
31
40
  };