@intentius/chant-lexicon-otel 0.81.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.
Files changed (206) hide show
  1. package/README.md +49 -0
  2. package/dist/catalog.d.ts +18 -0
  3. package/dist/catalog.d.ts.map +1 -0
  4. package/dist/codegen/docs-cli.d.ts +3 -0
  5. package/dist/codegen/docs-cli.d.ts.map +1 -0
  6. package/dist/codegen/docs.d.ts +8 -0
  7. package/dist/codegen/docs.d.ts.map +1 -0
  8. package/dist/codegen/generate-cli.d.ts +3 -0
  9. package/dist/codegen/generate-cli.d.ts.map +1 -0
  10. package/dist/codegen/generate.d.ts +19 -0
  11. package/dist/codegen/generate.d.ts.map +1 -0
  12. package/dist/codegen/package.d.ts +7 -0
  13. package/dist/codegen/package.d.ts.map +1 -0
  14. package/dist/collector.d.ts +29 -0
  15. package/dist/collector.d.ts.map +1 -0
  16. package/dist/components/common.d.ts +144 -0
  17. package/dist/components/common.d.ts.map +1 -0
  18. package/dist/components/exporters.d.ts +86 -0
  19. package/dist/components/exporters.d.ts.map +1 -0
  20. package/dist/components/extensions.d.ts +32 -0
  21. package/dist/components/extensions.d.ts.map +1 -0
  22. package/dist/components/index.d.ts +6 -0
  23. package/dist/components/index.d.ts.map +1 -0
  24. package/dist/components/processors.d.ts +118 -0
  25. package/dist/components/processors.d.ts.map +1 -0
  26. package/dist/components/receivers.d.ts +118 -0
  27. package/dist/components/receivers.d.ts.map +1 -0
  28. package/dist/define.d.ts +136 -0
  29. package/dist/define.d.ts.map +1 -0
  30. package/dist/detect.d.ts +2 -0
  31. package/dist/detect.d.ts.map +1 -0
  32. package/dist/index.d.ts +12 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/integrity.json +25 -0
  35. package/dist/lint/audit-catalog.d.ts +13 -0
  36. package/dist/lint/audit-catalog.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts +3 -0
  38. package/dist/lint/post-synth/index.d.ts.map +1 -0
  39. package/dist/lint/post-synth/otel-helpers.d.ts +22 -0
  40. package/dist/lint/post-synth/otel-helpers.d.ts.map +1 -0
  41. package/dist/lint/post-synth/otel101.d.ts +8 -0
  42. package/dist/lint/post-synth/otel101.d.ts.map +1 -0
  43. package/dist/lint/post-synth/otel102.d.ts +8 -0
  44. package/dist/lint/post-synth/otel102.d.ts.map +1 -0
  45. package/dist/lint/post-synth/otel103.d.ts +8 -0
  46. package/dist/lint/post-synth/otel103.d.ts.map +1 -0
  47. package/dist/lint/post-synth/otel104.d.ts +8 -0
  48. package/dist/lint/post-synth/otel104.d.ts.map +1 -0
  49. package/dist/lint/post-synth/otel105.d.ts +8 -0
  50. package/dist/lint/post-synth/otel105.d.ts.map +1 -0
  51. package/dist/lint/post-synth/otel106.d.ts +8 -0
  52. package/dist/lint/post-synth/otel106.d.ts.map +1 -0
  53. package/dist/lint/post-synth/otel107.d.ts +8 -0
  54. package/dist/lint/post-synth/otel107.d.ts.map +1 -0
  55. package/dist/lint/post-synth/otel108.d.ts +8 -0
  56. package/dist/lint/post-synth/otel108.d.ts.map +1 -0
  57. package/dist/lint/post-synth/otel109.d.ts +8 -0
  58. package/dist/lint/post-synth/otel109.d.ts.map +1 -0
  59. package/dist/lint/rules/component-id-syntax.d.ts +14 -0
  60. package/dist/lint/rules/component-id-syntax.d.ts.map +1 -0
  61. package/dist/lint/rules/index.d.ts +6 -0
  62. package/dist/lint/rules/index.d.ts.map +1 -0
  63. package/dist/lint/rules/literal-credential.d.ts +13 -0
  64. package/dist/lint/rules/literal-credential.d.ts.map +1 -0
  65. package/dist/lint/rules/otel-ast.d.ts +14 -0
  66. package/dist/lint/rules/otel-ast.d.ts.map +1 -0
  67. package/dist/lsp/completions.d.ts +4 -0
  68. package/dist/lsp/completions.d.ts.map +1 -0
  69. package/dist/lsp/hover.d.ts +4 -0
  70. package/dist/lsp/hover.d.ts.map +1 -0
  71. package/dist/manifest.json +8 -0
  72. package/dist/meta.json +102 -0
  73. package/dist/model.d.ts +53 -0
  74. package/dist/model.d.ts.map +1 -0
  75. package/dist/okf/index.md +40 -0
  76. package/dist/okf/rules/OTEL001.md +15 -0
  77. package/dist/okf/rules/OTEL002.md +11 -0
  78. package/dist/okf/rules/OTEL101.md +11 -0
  79. package/dist/okf/rules/OTEL102.md +11 -0
  80. package/dist/okf/rules/OTEL103.md +11 -0
  81. package/dist/okf/rules/OTEL104.md +11 -0
  82. package/dist/okf/rules/OTEL105.md +11 -0
  83. package/dist/okf/rules/OTEL106.md +11 -0
  84. package/dist/okf/rules/OTEL107.md +11 -0
  85. package/dist/okf/rules/OTEL108.md +11 -0
  86. package/dist/okf/rules/OTEL109.md +11 -0
  87. package/dist/okf/types/AttributesProcessor.md +9 -0
  88. package/dist/okf/types/BatchProcessor.md +9 -0
  89. package/dist/okf/types/DebugExporter.md +9 -0
  90. package/dist/okf/types/FileLogReceiver.md +9 -0
  91. package/dist/okf/types/GoogleCloudExporter.md +9 -0
  92. package/dist/okf/types/HealthCheckExtension.md +9 -0
  93. package/dist/okf/types/HostMetricsReceiver.md +9 -0
  94. package/dist/okf/types/K8sAttributesProcessor.md +9 -0
  95. package/dist/okf/types/MemoryLimiterProcessor.md +9 -0
  96. package/dist/okf/types/OtlpExporter.md +9 -0
  97. package/dist/okf/types/OtlpHttpExporter.md +9 -0
  98. package/dist/okf/types/OtlpReceiver.md +9 -0
  99. package/dist/okf/types/Pipeline.md +13 -0
  100. package/dist/okf/types/PprofExtension.md +9 -0
  101. package/dist/okf/types/PrometheusExporter.md +9 -0
  102. package/dist/okf/types/PrometheusReceiver.md +9 -0
  103. package/dist/okf/types/ResourceDetectionProcessor.md +9 -0
  104. package/dist/okf/types/ResourceProcessor.md +9 -0
  105. package/dist/okf/types/Service.md +9 -0
  106. package/dist/okf/types/ZPagesExtension.md +9 -0
  107. package/dist/package-cli.d.ts +3 -0
  108. package/dist/package-cli.d.ts.map +1 -0
  109. package/dist/pipeline.d.ts +66 -0
  110. package/dist/pipeline.d.ts.map +1 -0
  111. package/dist/platform.d.ts +57 -0
  112. package/dist/platform.d.ts.map +1 -0
  113. package/dist/plugin.d.ts +11 -0
  114. package/dist/plugin.d.ts.map +1 -0
  115. package/dist/rules/component-id-syntax.ts +68 -0
  116. package/dist/rules/literal-credential.ts +62 -0
  117. package/dist/rules/otel-ast.ts +30 -0
  118. package/dist/rules/otel-helpers.ts +70 -0
  119. package/dist/rules/otel101.ts +17 -0
  120. package/dist/rules/otel102.ts +17 -0
  121. package/dist/rules/otel103.ts +17 -0
  122. package/dist/rules/otel104.ts +17 -0
  123. package/dist/rules/otel105.ts +17 -0
  124. package/dist/rules/otel106.ts +17 -0
  125. package/dist/rules/otel107.ts +17 -0
  126. package/dist/rules/otel108.ts +17 -0
  127. package/dist/rules/otel109.ts +17 -0
  128. package/dist/serializer.d.ts +16 -0
  129. package/dist/serializer.d.ts.map +1 -0
  130. package/dist/skill-defs.d.ts +3 -0
  131. package/dist/skill-defs.d.ts.map +1 -0
  132. package/dist/skills/chant-otel-custom-components.md +51 -0
  133. package/dist/skills/chant-otel-platforms.md +34 -0
  134. package/dist/skills/chant-otel.md +60 -0
  135. package/dist/topology.d.ts +56 -0
  136. package/dist/topology.d.ts.map +1 -0
  137. package/dist/types/index.d.ts +3 -0
  138. package/dist/validate-cli.d.ts +3 -0
  139. package/dist/validate-cli.d.ts.map +1 -0
  140. package/dist/validate-config.d.ts +30 -0
  141. package/dist/validate-config.d.ts.map +1 -0
  142. package/dist/validate.d.ts +11 -0
  143. package/dist/validate.d.ts.map +1 -0
  144. package/dist/yaml.d.ts +27 -0
  145. package/dist/yaml.d.ts.map +1 -0
  146. package/package.json +74 -0
  147. package/src/catalog.ts +56 -0
  148. package/src/codegen/docs-cli.ts +4 -0
  149. package/src/codegen/docs.ts +90 -0
  150. package/src/codegen/generate-cli.ts +8 -0
  151. package/src/codegen/generate.ts +50 -0
  152. package/src/codegen/package.ts +41 -0
  153. package/src/collector.ts +147 -0
  154. package/src/components/common.ts +151 -0
  155. package/src/components/exporters.ts +141 -0
  156. package/src/components/extensions.ts +51 -0
  157. package/src/components/index.ts +5 -0
  158. package/src/components/processors.ts +208 -0
  159. package/src/components/receivers.ts +177 -0
  160. package/src/define.test.ts +131 -0
  161. package/src/define.ts +249 -0
  162. package/src/detect.ts +11 -0
  163. package/src/generated/lexicon-otel.json +102 -0
  164. package/src/index.ts +73 -0
  165. package/src/lint/audit-catalog.ts +102 -0
  166. package/src/lint/post-synth/index.ts +23 -0
  167. package/src/lint/post-synth/otel-helpers.ts +70 -0
  168. package/src/lint/post-synth/otel101.ts +17 -0
  169. package/src/lint/post-synth/otel102.ts +17 -0
  170. package/src/lint/post-synth/otel103.ts +17 -0
  171. package/src/lint/post-synth/otel104.ts +17 -0
  172. package/src/lint/post-synth/otel105.ts +17 -0
  173. package/src/lint/post-synth/otel106.ts +17 -0
  174. package/src/lint/post-synth/otel107.ts +17 -0
  175. package/src/lint/post-synth/otel108.ts +17 -0
  176. package/src/lint/post-synth/otel109.ts +17 -0
  177. package/src/lint/post-synth/post-synth.test.ts +184 -0
  178. package/src/lint/rules/component-id-syntax.ts +68 -0
  179. package/src/lint/rules/index.ts +9 -0
  180. package/src/lint/rules/literal-credential.ts +62 -0
  181. package/src/lint/rules/otel-ast.ts +30 -0
  182. package/src/lint/rules/rules.test.ts +70 -0
  183. package/src/lsp/completions.test.ts +23 -0
  184. package/src/lsp/completions.ts +15 -0
  185. package/src/lsp/hover.test.ts +20 -0
  186. package/src/lsp/hover.ts +30 -0
  187. package/src/model.ts +88 -0
  188. package/src/package-cli.ts +17 -0
  189. package/src/pipeline.ts +89 -0
  190. package/src/platform.test.ts +79 -0
  191. package/src/platform.ts +143 -0
  192. package/src/plugin.test.ts +55 -0
  193. package/src/plugin.ts +111 -0
  194. package/src/serializer.test.ts +187 -0
  195. package/src/serializer.ts +31 -0
  196. package/src/skill-defs.ts +36 -0
  197. package/src/skills/chant-otel-custom-components.md +51 -0
  198. package/src/skills/chant-otel-platforms.md +34 -0
  199. package/src/skills/chant-otel.md +60 -0
  200. package/src/topology.test.ts +80 -0
  201. package/src/topology.ts +122 -0
  202. package/src/validate-cli.ts +7 -0
  203. package/src/validate-config.ts +232 -0
  204. package/src/validate.ts +82 -0
  205. package/src/yaml.test.ts +56 -0
  206. package/src/yaml.ts +145 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * What the platform composites share: a default collector config, the image
3
+ * that runs it, where the config is mounted, and the ports a rendered config
4
+ * listens on.
5
+ *
6
+ * The docker, k8s and fly collector composites build on this. A composite
7
+ * declares its config with this lexicon, renders it with `collectorYaml`, and
8
+ * reads the listening ports back from the built config, so the ports it
9
+ * publishes follow whatever config the caller declared.
10
+ */
11
+
12
+ import type { Declarable } from "@intentius/chant/declarable";
13
+ import { COLLECTOR_PIN, type OTelComponent } from "./define";
14
+ import { OtlpReceiver } from "./components/receivers";
15
+ import { BatchProcessor, MemoryLimiterProcessor } from "./components/processors";
16
+ import { DebugExporter } from "./components/exporters";
17
+ import { HealthCheckExtension } from "./components/extensions";
18
+ import { Pipeline } from "./pipeline";
19
+ import { SIGNALS, parseComponentId, type CollectorConfig, type Signal } from "./model";
20
+
21
+ /** The contrib collector image at the version the built-in components are typed against. */
22
+ export const COLLECTOR_IMAGE = `otel/opentelemetry-collector-contrib:${COLLECTOR_PIN.version.replace(/^v/, "")}`;
23
+
24
+ /** Where the platform composites mount the rendered config inside the container. */
25
+ export const COLLECTOR_CONFIG_PATH = "/etc/otel/config.yaml";
26
+
27
+ export interface OtlpCollectorOptions {
28
+ /** Where telemetry goes. Default: one `debug` exporter at `basic` verbosity. */
29
+ exporters?: OTelComponent<"exporter", string, any>[];
30
+ /** Which signals get a pipeline. Default: traces, metrics and logs. */
31
+ signals?: Signal[];
32
+ /** Serve `health_check` on 0.0.0.0:13133. Default: true. */
33
+ healthCheck?: boolean;
34
+ }
35
+
36
+ /**
37
+ * The entities of a small OTLP collector: an `otlp` receiver on 4317 (gRPC)
38
+ * and 4318 (HTTP), `memory_limiter` then `batch`, the given exporters, and a
39
+ * `health_check` extension. Pass the result to `collectorYaml`.
40
+ */
41
+ export function otlpCollector(options: OtlpCollectorOptions = {}): Declarable[] {
42
+ const { exporters = [new DebugExporter({ verbosity: "basic" })], signals = [...SIGNALS], healthCheck = true } = options;
43
+ const otlp = new OtlpReceiver({
44
+ protocols: {
45
+ grpc: { endpoint: "0.0.0.0:4317" },
46
+ http: { endpoint: "0.0.0.0:4318" },
47
+ },
48
+ });
49
+ const memoryLimiter = new MemoryLimiterProcessor({
50
+ check_interval: "1s",
51
+ limit_percentage: 80,
52
+ spike_limit_percentage: 20,
53
+ });
54
+ const batch = new BatchProcessor({});
55
+ const entities: Declarable[] = [otlp, memoryLimiter, batch, ...exporters];
56
+ if (healthCheck) entities.push(new HealthCheckExtension({ endpoint: "0.0.0.0:13133" }));
57
+ for (const signal of signals) {
58
+ entities.push(new Pipeline({ signal, receivers: [otlp], processors: [memoryLimiter, batch], exporters }));
59
+ }
60
+ return entities;
61
+ }
62
+
63
+ /** A port a collector config listens on. */
64
+ export interface CollectorPort {
65
+ /** A name usable as a k8s port name: lowercase, `-` separated, at most 15 characters. */
66
+ name: string;
67
+ port: number;
68
+ }
69
+
70
+ export interface CollectorEndpoints {
71
+ /** Ports the receivers listen on, in config order, one entry per port. */
72
+ ports: CollectorPort[];
73
+ /**
74
+ * The `health_check` extension, when the service enables one and it listens
75
+ * on an address other than localhost. A probe from outside the container
76
+ * cannot reach a localhost listener.
77
+ */
78
+ healthCheck?: { port: number; path: string };
79
+ }
80
+
81
+ function portOf(endpoint: unknown): { host: string; port: number } | undefined {
82
+ if (typeof endpoint !== "string") return undefined;
83
+ const m = endpoint.match(/^(.*):(\d+)$/);
84
+ if (!m) return undefined;
85
+ return { host: m[1], port: Number(m[2]) };
86
+ }
87
+
88
+ function portName(parts: string[]): string {
89
+ const name = parts
90
+ .join("-")
91
+ .toLowerCase()
92
+ .replace(/[^a-z0-9-]+/g, "-")
93
+ .replace(/-+/g, "-")
94
+ .replace(/^-|-$/g, "");
95
+ return name.slice(0, 15).replace(/-$/, "");
96
+ }
97
+
98
+ /** Every `endpoint` value under a receiver's config, with the key path that led to it. */
99
+ function receiverEndpoints(value: unknown, path: string[], out: Array<{ path: string[]; endpoint: unknown }>): void {
100
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
101
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
102
+ if (key === "endpoint") out.push({ path, endpoint: child });
103
+ else receiverEndpoints(child, [...path, key], out);
104
+ }
105
+ }
106
+
107
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
108
+
109
+ /**
110
+ * The ports a built collector config listens on: every receiver `endpoint`,
111
+ * named after the receiver and the protocol key above it (`otlp-grpc`,
112
+ * `otlp-http`), and the `health_check` port if the service enables it.
113
+ */
114
+ export function collectorEndpoints(config: CollectorConfig): CollectorEndpoints {
115
+ const ports: CollectorPort[] = [];
116
+ const seen = new Set<number>();
117
+ for (const [id, receiverConfig] of Object.entries(config.receivers ?? {})) {
118
+ const found: Array<{ path: string[]; endpoint: unknown }> = [];
119
+ receiverEndpoints(receiverConfig, [], found);
120
+ const parsed = parseComponentId(id);
121
+ const base = parsed ? [parsed.type, ...(parsed.name ? [parsed.name] : [])] : [id];
122
+ for (const { path, endpoint } of found) {
123
+ const p = portOf(endpoint);
124
+ if (!p || seen.has(p.port)) continue;
125
+ seen.add(p.port);
126
+ const protocol = path.filter((k) => k !== "protocols");
127
+ ports.push({ name: portName([...base, ...protocol]), port: p.port });
128
+ }
129
+ }
130
+
131
+ let healthCheck: CollectorEndpoints["healthCheck"];
132
+ const enabled = config.service?.extensions ?? [];
133
+ for (const id of enabled) {
134
+ if (parseComponentId(id)?.type !== "health_check") continue;
135
+ const ext = (config.extensions?.[id] ?? {}) as { endpoint?: unknown; path?: unknown };
136
+ const p = portOf(ext.endpoint ?? "localhost:13133");
137
+ if (!p || LOCAL_HOSTS.has(p.host)) continue;
138
+ healthCheck = { port: p.port, path: typeof ext.path === "string" ? ext.path : "/" };
139
+ break;
140
+ }
141
+
142
+ return { ports, ...(healthCheck ? { healthCheck } : {}) };
143
+ }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isLexiconPlugin } from "@intentius/chant/lexicon";
3
+ import { otelPlugin } from "./plugin";
4
+ import { otelAuditCatalog } from "./lint/audit-catalog";
5
+
6
+ describe("otel plugin", () => {
7
+ it("is a valid LexiconPlugin", () => {
8
+ expect(isLexiconPlugin(otelPlugin)).toBe(true);
9
+ });
10
+
11
+ it("is named otel and serializes under the OTEL prefix", () => {
12
+ expect(otelPlugin.name).toBe("otel");
13
+ expect(otelPlugin.serializer.name).toBe("otel");
14
+ expect(otelPlugin.serializer.rulePrefix).toBe("OTEL");
15
+ });
16
+
17
+ it("ships lint rules and post-synth checks, all under the OTEL prefix", () => {
18
+ const ids = [...otelPlugin.lintRules!().map((r) => r.id), ...otelPlugin.postSynthChecks!().map((c) => c.id)];
19
+ expect(ids).toEqual([
20
+ "OTEL001",
21
+ "OTEL002",
22
+ "OTEL101",
23
+ "OTEL102",
24
+ "OTEL103",
25
+ "OTEL104",
26
+ "OTEL105",
27
+ "OTEL106",
28
+ "OTEL107",
29
+ "OTEL108",
30
+ "OTEL109",
31
+ ]);
32
+ for (const id of ids) expect(id.startsWith("OTEL")).toBe(true);
33
+ });
34
+
35
+ it("catalogues every post-synth check for chant audit", () => {
36
+ for (const check of otelPlugin.postSynthChecks!()) expect(otelAuditCatalog[check.id]).toBeDefined();
37
+ });
38
+
39
+ it("detects a collector config and nothing else", () => {
40
+ expect(otelPlugin.detectTemplate!({ receivers: { otlp: {} }, service: { pipelines: { traces: {} } } })).toBe(true);
41
+ expect(otelPlugin.detectTemplate!({ apiVersion: "v1", kind: "ConfigMap" })).toBe(false);
42
+ expect(otelPlugin.detectTemplate!({ services: { web: {} } })).toBe(false);
43
+ });
44
+
45
+ it("loads its skills with content", () => {
46
+ const skills = otelPlugin.skills!();
47
+ expect(skills.map((s) => s.name)).toEqual(["chant-otel", "chant-otel-custom-components", "chant-otel-platforms"]);
48
+ for (const s of skills) expect(s.content.length).toBeGreaterThan(100);
49
+ });
50
+
51
+ it("registers namespaced MCP contributions", () => {
52
+ expect(otelPlugin.mcpTools!().map((t) => t.name)).toEqual(["otel:diff"]);
53
+ expect(otelPlugin.mcpResources!().map((r) => r.uri)).toEqual(["otel:resource-catalog"]);
54
+ });
55
+ });
package/src/plugin.ts ADDED
@@ -0,0 +1,111 @@
1
+ import type { LexiconPlugin } from "@intentius/chant/lexicon";
2
+ import type { CompletionContext, HoverContext } from "@intentius/chant/lsp/types";
3
+ import type { McpResourceContribution } from "@intentius/chant/mcp/types";
4
+ import { createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
5
+ import { otelSerializer } from "./serializer";
6
+ import { rules } from "./lint/rules";
7
+ import { postSynthChecks } from "./lint/post-synth";
8
+ import { otelAuditCatalog } from "./lint/audit-catalog";
9
+ import { completions } from "./lsp/completions";
10
+ import { hover } from "./lsp/hover";
11
+ import { detectTemplate } from "./detect";
12
+ import { otelSkills } from "./skill-defs";
13
+ import { BUILTIN_CATALOG } from "./catalog";
14
+ import { COLLECTOR_PIN } from "./define";
15
+
16
+ const catalogResource: McpResourceContribution = {
17
+ uri: "otel:resource-catalog",
18
+ name: "OpenTelemetry Collector component catalog",
19
+ description: "The collector components, pipelines and service block this lexicon types, with the collector release they follow",
20
+ mimeType: "application/json",
21
+ async handler(): Promise<string> {
22
+ return JSON.stringify({ pin: COLLECTOR_PIN, entities: BUILTIN_CATALOG });
23
+ },
24
+ };
25
+
26
+ /**
27
+ * OpenTelemetry Collector lexicon plugin.
28
+ *
29
+ * Typed receivers, processors, exporters and extensions, pipelines and the
30
+ * service block, serialized to one collector config file. A component chant
31
+ * doesn't ship comes in through `defineComponent`, and is serialized and
32
+ * checked the same way as the built-ins.
33
+ */
34
+ export const otelPlugin: LexiconPlugin = {
35
+ name: "otel",
36
+ serializer: otelSerializer,
37
+
38
+ // ── Required lifecycle methods ────────────────────────────────
39
+
40
+ async generate(options?: { verbose?: boolean }): Promise<void> {
41
+ const { generate, writeGeneratedFiles } = await import("./codegen/generate");
42
+ writeGeneratedFiles(await generate(options));
43
+ },
44
+
45
+ async validate(_options?: { verbose?: boolean }): Promise<void> {
46
+ const { validate } = await import("./validate");
47
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
48
+ printValidationResult(await validate());
49
+ },
50
+
51
+ async coverage(_options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
52
+ const components = BUILTIN_CATALOG.filter((c) => c.type !== undefined);
53
+ const byKind = new Map<string, string[]>();
54
+ for (const c of components) byKind.set(c.kind, [...(byKind.get(c.kind) ?? []), c.type!]);
55
+ console.error(`otel: ${components.length} built-in components, typed against ${COLLECTOR_PIN.source} ${COLLECTOR_PIN.version}`);
56
+ for (const [kind, types] of byKind) console.error(` ${kind}s: ${types.join(", ")}`);
57
+ console.error(" anything else: defineComponent (see docs: custom components)");
58
+ },
59
+
60
+ async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
61
+ const { packageLexicon } = await import("./codegen/package");
62
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
63
+ const { join, dirname } = await import("path");
64
+ const { fileURLToPath } = await import("url");
65
+ const { spec, stats } = await packageLexicon(options);
66
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
67
+ writeBundleSpec(spec, join(pkgDir, "dist"));
68
+ console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
69
+ },
70
+
71
+ // ── Optional extensions ────────────────────────────────────
72
+
73
+ lintRules() {
74
+ return rules;
75
+ },
76
+
77
+ postSynthChecks() {
78
+ return postSynthChecks;
79
+ },
80
+
81
+ auditCatalog() {
82
+ return otelAuditCatalog;
83
+ },
84
+
85
+ skills: otelSkills,
86
+
87
+ mcpTools() {
88
+ return [createDiffTool(otelSerializer, "Compare current collector config output against the previous build", "otel")];
89
+ },
90
+
91
+ mcpResources() {
92
+ return [catalogResource];
93
+ },
94
+
95
+ detectTemplate(data: unknown) {
96
+ return detectTemplate(data);
97
+ },
98
+
99
+ completionProvider(ctx: CompletionContext) {
100
+ return completions(ctx);
101
+ },
102
+
103
+ hoverProvider(ctx: HoverContext) {
104
+ return hover(ctx);
105
+ },
106
+
107
+ async docs(options?: { verbose?: boolean }) {
108
+ const { generateDocs } = await import("./codegen/docs");
109
+ return generateDocs(options);
110
+ },
111
+ };
@@ -0,0 +1,187 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { load } from "js-yaml";
3
+ import type { Declarable } from "@intentius/chant/declarable";
4
+ import { otelSerializer } from "./serializer";
5
+ import {
6
+ BatchProcessor,
7
+ DebugExporter,
8
+ GoogleCloudExporter,
9
+ HealthCheckExtension,
10
+ MemoryLimiterProcessor,
11
+ OtlpExporter,
12
+ OtlpReceiver,
13
+ Pipeline,
14
+ PprofExtension,
15
+ Service,
16
+ } from "./index";
17
+
18
+ function entities(record: Record<string, unknown>): Map<string, Declarable> {
19
+ return new Map(Object.entries(record) as Array<[string, Declarable]>);
20
+ }
21
+
22
+ function primary(out: ReturnType<typeof otelSerializer.serialize>): string {
23
+ return typeof out === "string" ? out : out.primary;
24
+ }
25
+
26
+ describe("otel serializer", () => {
27
+ // 1, 2
28
+ test("name and rule prefix", () => {
29
+ expect(otelSerializer.name).toBe("otel");
30
+ expect(otelSerializer.rulePrefix).toBe("OTEL");
31
+ });
32
+
33
+ // 3
34
+ test("an empty map serializes to the empty string", () => {
35
+ expect(otelSerializer.serialize(new Map())).toBe("");
36
+ });
37
+
38
+ // 4
39
+ test("a minimal traces pipeline", () => {
40
+ const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
41
+ const debug = new DebugExporter({ verbosity: "basic" });
42
+ const out = otelSerializer.serialize(
43
+ entities({ otlp, debug, traces: new Pipeline({ signal: "traces", receivers: [otlp], exporters: [debug] }) }),
44
+ );
45
+ expect(out).toBe(`receivers:
46
+ otlp:
47
+ protocols:
48
+ grpc:
49
+ endpoint: 0.0.0.0:4317
50
+
51
+ exporters:
52
+ debug:
53
+ verbosity: basic
54
+
55
+ service:
56
+ pipelines:
57
+ traces:
58
+ receivers: [otlp]
59
+ exporters: [debug]
60
+ `);
61
+ });
62
+
63
+ // 5, 6: the collector id comes from the type and the instance name, never the export name
64
+ test("the export name does not leak into the id; an instance name makes type/name", () => {
65
+ const tempo = new OtlpExporter({ name: "tempo", endpoint: "tempo:4317" });
66
+ const plain = new OtlpExporter({ endpoint: "other:4317" });
67
+ const yaml = primary(otelSerializer.serialize(entities({ myTempoExporter: tempo, anotherOne: plain })));
68
+ const parsed = load(yaml) as any;
69
+ expect(Object.keys(parsed.exporters)).toEqual(["otlp/tempo", "otlp"]);
70
+ expect(yaml).not.toContain("myTempoExporter");
71
+ expect(parsed.exporters["otlp/tempo"]).toEqual({ endpoint: "tempo:4317" });
72
+ });
73
+
74
+ // 7
75
+ test("several pipelines, in declaration order, sharing components", () => {
76
+ const otlp = new OtlpReceiver({ protocols: { grpc: {}, http: {} } });
77
+ const batch = new BatchProcessor({});
78
+ const debug = new DebugExporter({});
79
+ const parsed = load(
80
+ primary(
81
+ otelSerializer.serialize(
82
+ entities({
83
+ otlp,
84
+ batch,
85
+ debug,
86
+ logs: new Pipeline({ signal: "logs", receivers: [otlp], processors: [batch], exporters: [debug] }),
87
+ traces: new Pipeline({ signal: "traces", name: "debug", receivers: [otlp], exporters: [debug] }),
88
+ }),
89
+ ),
90
+ ),
91
+ ) as any;
92
+ expect(Object.keys(parsed.service.pipelines)).toEqual(["logs", "traces/debug"]);
93
+ expect(parsed.service.pipelines["traces/debug"].processors).toBeUndefined();
94
+ expect(parsed.receivers.otlp).toEqual({ protocols: { grpc: {}, http: {} } });
95
+ });
96
+
97
+ // 8
98
+ test("declared extensions are enabled by default, in declaration order", () => {
99
+ const health = new HealthCheckExtension({ endpoint: "0.0.0.0:13133" });
100
+ const pprof = new PprofExtension({});
101
+ const parsed = load(primary(otelSerializer.serialize(entities({ health, pprof })))) as any;
102
+ expect(parsed.service.extensions).toEqual(["health_check", "pprof"]);
103
+ });
104
+
105
+ // 9
106
+ test("a Service that lists extensions wins over the default, and carries telemetry", () => {
107
+ const health = new HealthCheckExtension({});
108
+ const pprof = new PprofExtension({});
109
+ const parsed = load(
110
+ primary(
111
+ otelSerializer.serialize(
112
+ entities({ health, pprof, service: new Service({ extensions: [health], telemetry: { logs: { level: "warn" } } }) }),
113
+ ),
114
+ ),
115
+ ) as any;
116
+ expect(parsed.service.extensions).toEqual(["health_check"]);
117
+ expect(parsed.service.telemetry).toEqual({ logs: { level: "warn" } });
118
+ });
119
+
120
+ // 10
121
+ test("a component a pipeline references but nobody listed is still emitted", () => {
122
+ const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
123
+ const debug = new DebugExporter({});
124
+ const parsed = load(
125
+ primary(otelSerializer.serialize(entities({ traces: new Pipeline({ signal: "traces", receivers: [otlp], exporters: [debug] }) }))),
126
+ ) as any;
127
+ expect(Object.keys(parsed.receivers)).toEqual(["otlp"]);
128
+ expect(Object.keys(parsed.exporters)).toEqual(["debug"]);
129
+ });
130
+
131
+ // 11
132
+ test("sections come out in collector order whatever the declaration order", () => {
133
+ const debug = new DebugExporter({});
134
+ const health = new HealthCheckExtension({});
135
+ const limiter = new MemoryLimiterProcessor({ check_interval: "1s", limit_mib: 400 });
136
+ const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
137
+ const yaml = primary(
138
+ otelSerializer.serialize(
139
+ entities({
140
+ traces: new Pipeline({ signal: "traces", receivers: [otlp], processors: [limiter], exporters: [debug] }),
141
+ debug,
142
+ health,
143
+ limiter,
144
+ otlp,
145
+ }),
146
+ ),
147
+ );
148
+ const sections = yaml.split("\n").filter((l) => /^[a-z]/.test(l));
149
+ expect(sections).toEqual(["receivers:", "processors:", "exporters:", "extensions:", "service:"]);
150
+ });
151
+
152
+ // 12a: the output parses back to what was declared
153
+ test("round-trips through a YAML parser, including values that need quoting", () => {
154
+ const exporter = new OtlpExporter({
155
+ name: "vendor",
156
+ endpoint: "api.vendor.example:443",
157
+ headers: { "x-api-key": "${env:VENDOR_KEY}", "x-tenant": "true", "x-shard": "0012" },
158
+ compression: "gzip",
159
+ retry_on_failure: { enabled: true, max_elapsed_time: "300s" },
160
+ });
161
+ const gc = new GoogleCloudExporter({ project: "my-project", metric: { known_domains: ["a,b", "c"] } });
162
+ const parsed = load(primary(otelSerializer.serialize(entities({ exporter, gc })))) as any;
163
+ expect(parsed.exporters["otlp/vendor"].headers).toEqual({
164
+ "x-api-key": "${env:VENDOR_KEY}",
165
+ "x-tenant": "true",
166
+ "x-shard": "0012",
167
+ });
168
+ expect(parsed.exporters["otlp/vendor"].retry_on_failure).toEqual({ enabled: true, max_elapsed_time: "300s" });
169
+ expect(parsed.exporters.googlecloud.metric.known_domains).toEqual(["a,b", "c"]);
170
+ });
171
+
172
+ // 12b
173
+ test("two components with one id: the first is emitted and a warning says so", () => {
174
+ const a = new DebugExporter({ verbosity: "basic" });
175
+ const b = new DebugExporter({ verbosity: "detailed" });
176
+ const out = otelSerializer.serialize(entities({ a, b }));
177
+ expect(typeof out).toBe("object");
178
+ const result = out as { primary: string; warnings?: string[] };
179
+ expect((load(result.primary) as any).exporters.debug).toEqual({ verbosity: "basic" });
180
+ expect(result.warnings?.[0]).toContain('declare the id "debug"');
181
+ });
182
+
183
+ test("non-otel entities are ignored", () => {
184
+ const foreign = { lexicon: "k8s", entityType: "K8s::Core::ConfigMap", kind: "resource", props: {} } as unknown as Declarable;
185
+ expect(otelSerializer.serialize(entities({ foreign }))).toBe("");
186
+ });
187
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OpenTelemetry Collector serializer.
3
+ *
4
+ * Emits one collector config file from every otel entity in the build: the
5
+ * YAML `otelcol --config` reads as it is. Section order is fixed (receivers,
6
+ * processors, exporters, extensions, service); components and pipelines keep
7
+ * the order they are declared in, and each component's config keeps the key
8
+ * order it was written in, since collector docs and diffs read that way.
9
+ *
10
+ * A collector config has no metadata channel, so there is no ownership marker
11
+ * to stamp. A custom component's schema pin is written as a `# chant:` comment
12
+ * line above the config (see `define.ts`).
13
+ */
14
+
15
+ import type { Declarable } from "@intentius/chant/declarable";
16
+ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
17
+ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
18
+ import { buildCollectorConfig } from "./collector";
19
+ import { emitCollectorYaml } from "./yaml";
20
+
21
+ export const otelSerializer: Serializer = {
22
+ name: "otel",
23
+ rulePrefix: "OTEL",
24
+
25
+ serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string | SerializerResult {
26
+ const built = buildCollectorConfig(entities);
27
+ const yaml = emitCollectorYaml(built.config, { header: built.header });
28
+ if (built.warnings.length === 0) return yaml;
29
+ return { primary: yaml, warnings: built.warnings };
30
+ },
31
+ };
@@ -0,0 +1,36 @@
1
+ import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
2
+
3
+ /** The otel lexicon's AI skills, read from src/skills/. */
4
+ export const otelSkills = createSkillsLoader(import.meta.url, [
5
+ {
6
+ file: "chant-otel.md",
7
+ name: "chant-otel",
8
+ description: "Declare OpenTelemetry Collector config (receivers, processors, exporters, pipelines) as typed chant entities and build collector YAML",
9
+ triggers: [
10
+ { type: "context" as const, value: "opentelemetry" },
11
+ { type: "context" as const, value: "otel collector" },
12
+ { type: "file-pattern" as const, value: "*.otel.ts" },
13
+ ],
14
+ examples: [
15
+ {
16
+ title: "An OTLP-in, OTLP-out traces pipeline",
17
+ output:
18
+ 'const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });\n' +
19
+ 'const backend = new OtlpExporter({ name: "backend", endpoint: "tempo:4317" });\n' +
20
+ "export const traces = new Pipeline({ signal: \"traces\", receivers: [otlp], exporters: [backend] });",
21
+ },
22
+ ],
23
+ },
24
+ {
25
+ file: "chant-otel-custom-components.md",
26
+ name: "chant-otel-custom-components",
27
+ description: "Add a collector component chant doesn't ship (a vendor exporter, an in-house processor) with defineComponent, and pin its schema",
28
+ triggers: [{ type: "context" as const, value: "otel custom component" }],
29
+ },
30
+ {
31
+ file: "chant-otel-platforms.md",
32
+ name: "chant-otel-platforms",
33
+ description: "Run a declared collector config on Kubernetes, GKE, Docker or Fly by rendering it into the platform's own config file",
34
+ triggers: [{ type: "context" as const, value: "deploy otel collector" }],
35
+ },
36
+ ]);
@@ -0,0 +1,51 @@
1
+ ---
2
+ skill: chant-otel-custom-components
3
+ description: Add a collector component chant doesn't ship (a vendor exporter, an in-house processor) with defineComponent, and pin its schema
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Custom collector components
8
+
9
+ chant ships a core set of collector components. For anything else, define the component once and use it like a built-in.
10
+
11
+ ```ts
12
+ import { defineComponent, Pipeline, OtlpReceiver } from "@intentius/chant-lexicon-otel";
13
+
14
+ export interface DatadogExporterConfig {
15
+ api: { key: string; site?: string };
16
+ traces?: { span_name_as_resource_name?: boolean };
17
+ }
18
+
19
+ export const DatadogExporter = defineComponent<DatadogExporterConfig>()({
20
+ kind: "exporter",
21
+ type: "datadog",
22
+ pin: {
23
+ source: "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter",
24
+ version: "v0.130.0",
25
+ },
26
+ validate: (c) => (c.api.key.includes("${") ? [] : ["api.key must come from ${env:...}"]),
27
+ endpoints: (c) => [`https://api.${c.api.site ?? "datadoghq.com"}`],
28
+ });
29
+
30
+ export const dd = new DatadogExporter({ name: "eu", api: { key: "${env:DD_API_KEY}", site: "datadoghq.eu" } });
31
+ export const traces = new Pipeline({ signal: "traces", receivers: [new OtlpReceiver({ protocols: { grpc: {} } })], exporters: [dd] });
32
+ ```
33
+
34
+ ## What you supply
35
+
36
+ | Field | Required | Meaning |
37
+ |---|---|---|
38
+ | `kind` | yes | receiver, processor, exporter or extension |
39
+ | `type` | yes | the collector type, the part of the id before `/` |
40
+ | `pin` | yes | `{ source, version, digest? }`: where the config schema comes from and which release the type follows |
41
+ | `validate` | no | a function returning problems, or any zod-compatible schema (anything with `safeParse`) |
42
+ | `endpoints` | no | where the component sends or listens, for `collectorTopology()` |
43
+ | `description` | no | one line for docs |
44
+
45
+ ## How it is checked and pinned
46
+
47
+ - The same serializer emits it, and OTEL101-OTEL108 apply to it as to any built-in. `validate` failures are OTEL107.
48
+ - The pin is written as a `# chant: exporter datadog/eu schema <source>@<version>` comment above the emitted config, and returned by `collectorTopology()`.
49
+ - A definition without a usable pin fails the build (OTEL109).
50
+ - chant records the pin; it does not fetch or verify the schema. Ship the definition in a package and the package version pins it for everyone who imports it.
51
+ - You cannot redefine a built-in type. Declare a named instance instead (`new OtlpExporter({ name: "vendor", ... })`).
@@ -0,0 +1,34 @@
1
+ ---
2
+ skill: chant-otel-platforms
3
+ description: Run a declared collector config on Kubernetes, GKE, Docker or Fly by rendering it into the platform's own config file
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Running the collector config on a platform
8
+
9
+ The otel lexicon produces collector YAML. A platform lexicon runs the collector and carries the YAML to it.
10
+
11
+ ## Rendering the YAML inside another lexicon
12
+
13
+ `collectorYaml(entities)` returns exactly what the otel serializer would emit for those entities. Use it wherever a platform needs the config as a string:
14
+
15
+ ```ts
16
+ import { OtlpReceiver, DebugExporter, Pipeline, collectorYaml } from "@intentius/chant-lexicon-otel";
17
+ import { ConfigMap } from "@intentius/chant-lexicon-k8s";
18
+
19
+ const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
20
+ const debug = new DebugExporter({ verbosity: "basic" });
21
+
22
+ export const collectorConfig = new ConfigMap({
23
+ metadata: { name: "otel-collector-config" },
24
+ data: { "config.yaml": collectorYaml([otlp, debug, new Pipeline({ signal: "traces", receivers: [otlp], exporters: [debug] })]) },
25
+ });
26
+ ```
27
+
28
+ ## GKE
29
+
30
+ The k8s lexicon's `GkeOtelCollector` composite is built this way: it declares an otlp receiver, batch and resourcedetection processors and a googlecloud exporter, renders them with `collectorYaml`, and mounts the result from a ConfigMap into a DaemonSet running under Workload Identity.
31
+
32
+ ## Checking a rendered config
33
+
34
+ `validateCollectorConfig(config)` runs OTEL101-OTEL106 over any parsed collector config, so a composite's generated YAML can be checked in its own tests.