@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,177 @@
1
+ /**
2
+ * Built-in receivers: otlp, prometheus, hostmetrics, filelog.
3
+ */
4
+
5
+ import { defineBuiltin } from "../define";
6
+ import type { Duration, GRPCServerSettings, HTTPServerSettings, TLSClientSettings } from "./common";
7
+
8
+ // ── otlp ─────────────────────────────────────────────────────────────
9
+
10
+ export interface OtlpReceiverConfig {
11
+ protocols?: {
12
+ grpc?: GRPCServerSettings | null;
13
+ http?:
14
+ | (HTTPServerSettings & {
15
+ traces_url_path?: string;
16
+ metrics_url_path?: string;
17
+ logs_url_path?: string;
18
+ })
19
+ | null;
20
+ };
21
+ }
22
+
23
+ /** Receives OTLP over gRPC (default port 4317) and HTTP (default port 4318). */
24
+ export const OtlpReceiver = defineBuiltin<OtlpReceiverConfig, "receiver", "otlp">({
25
+ kind: "receiver",
26
+ type: "otlp",
27
+ description: "Receives traces, metrics and logs over OTLP gRPC (4317) and HTTP (4318)",
28
+ validate: (c) =>
29
+ !c.protocols || (!("grpc" in c.protocols) && !("http" in c.protocols))
30
+ ? ["protocols enables neither grpc nor http, so the receiver listens on nothing"]
31
+ : [],
32
+ endpoints: (c) => {
33
+ const out: string[] = [];
34
+ if (c.protocols && "grpc" in c.protocols) out.push(c.protocols.grpc?.endpoint ?? "localhost:4317");
35
+ if (c.protocols && "http" in c.protocols) out.push(c.protocols.http?.endpoint ?? "localhost:4318");
36
+ return out;
37
+ },
38
+ });
39
+
40
+ // ── prometheus ───────────────────────────────────────────────────────
41
+
42
+ export interface PrometheusStaticConfig {
43
+ targets: string[];
44
+ labels?: Record<string, string>;
45
+ }
46
+
47
+ /** One Prometheus scrape job. Service-discovery and relabel blocks pass through as written. */
48
+ export interface PrometheusScrapeConfig {
49
+ job_name: string;
50
+ scrape_interval?: Duration;
51
+ scrape_timeout?: Duration;
52
+ metrics_path?: string;
53
+ scheme?: "http" | "https";
54
+ honor_labels?: boolean;
55
+ honor_timestamps?: boolean;
56
+ params?: Record<string, string[]>;
57
+ static_configs?: PrometheusStaticConfig[];
58
+ kubernetes_sd_configs?: Array<Record<string, unknown>>;
59
+ file_sd_configs?: Array<Record<string, unknown>>;
60
+ relabel_configs?: Array<Record<string, unknown>>;
61
+ metric_relabel_configs?: Array<Record<string, unknown>>;
62
+ tls_config?: Record<string, unknown>;
63
+ authorization?: { type?: string; credentials_file?: string };
64
+ bearer_token_file?: string;
65
+ sample_limit?: number;
66
+ }
67
+
68
+ export interface PrometheusReceiverConfig {
69
+ config: {
70
+ global?: { scrape_interval?: Duration; scrape_timeout?: Duration; evaluation_interval?: Duration; external_labels?: Record<string, string> };
71
+ scrape_configs: PrometheusScrapeConfig[];
72
+ };
73
+ trim_metric_suffixes?: boolean;
74
+ use_start_time_metric?: boolean;
75
+ start_time_metric_regex?: string;
76
+ target_allocator?: {
77
+ endpoint: string;
78
+ interval?: Duration;
79
+ collector_id?: string;
80
+ tls?: TLSClientSettings;
81
+ };
82
+ }
83
+
84
+ /** Scrapes Prometheus endpoints with Prometheus's own scrape config. */
85
+ export const PrometheusReceiver = defineBuiltin<PrometheusReceiverConfig, "receiver", "prometheus">({
86
+ kind: "receiver",
87
+ type: "prometheus",
88
+ description: "Scrapes Prometheus endpoints using Prometheus scrape_configs",
89
+ validate: (c) => {
90
+ const problems: string[] = [];
91
+ const seen = new Set<string>();
92
+ for (const job of c.config?.scrape_configs ?? []) {
93
+ if (seen.has(job.job_name)) problems.push(`scrape job "${job.job_name}" is declared twice`);
94
+ seen.add(job.job_name);
95
+ }
96
+ if ((c.config?.scrape_configs ?? []).length === 0 && !c.target_allocator) {
97
+ problems.push("config.scrape_configs is empty and no target_allocator is set, so nothing is scraped");
98
+ }
99
+ return problems;
100
+ },
101
+ endpoints: (c) =>
102
+ (c.config?.scrape_configs ?? []).flatMap((job) => (job.static_configs ?? []).flatMap((s) => s.targets)),
103
+ });
104
+
105
+ // ── hostmetrics ──────────────────────────────────────────────────────
106
+
107
+ export type HostMetricsScraper =
108
+ | "cpu"
109
+ | "disk"
110
+ | "load"
111
+ | "filesystem"
112
+ | "memory"
113
+ | "network"
114
+ | "paging"
115
+ | "processes"
116
+ | "process"
117
+ | "system";
118
+
119
+ export interface HostMetricsReceiverConfig {
120
+ collection_interval?: Duration;
121
+ initial_delay?: Duration;
122
+ /** Host root when the collector runs in a container, e.g. `/hostfs`. */
123
+ root_path?: string;
124
+ /** Scraper name to its settings; `{}` enables a scraper with its defaults. */
125
+ scrapers: Partial<Record<HostMetricsScraper, Record<string, unknown>>>;
126
+ }
127
+
128
+ /** Scrapes CPU, memory, disk, filesystem, network and process metrics from the host. */
129
+ export const HostMetricsReceiver = defineBuiltin<HostMetricsReceiverConfig, "receiver", "hostmetrics">({
130
+ kind: "receiver",
131
+ type: "hostmetrics",
132
+ description: "Scrapes host metrics (cpu, memory, disk, filesystem, network, processes)",
133
+ validate: (c) => (Object.keys(c.scrapers ?? {}).length === 0 ? ["scrapers is empty, so no host metric is collected"] : []),
134
+ });
135
+
136
+ // ── filelog ──────────────────────────────────────────────────────────
137
+
138
+ /** A stanza operator (`regex_parser`, `json_parser`, `move`, …). Its other keys depend on `type`. */
139
+ export interface FileLogOperator {
140
+ type: string;
141
+ id?: string;
142
+ output?: string | string[];
143
+ [key: string]: unknown;
144
+ }
145
+
146
+ export interface FileLogReceiverConfig {
147
+ include: string[];
148
+ exclude?: string[];
149
+ start_at?: "beginning" | "end";
150
+ include_file_name?: boolean;
151
+ include_file_path?: boolean;
152
+ include_file_name_resolved?: boolean;
153
+ include_file_path_resolved?: boolean;
154
+ poll_interval?: Duration;
155
+ max_concurrent_files?: number;
156
+ max_log_size?: string;
157
+ fingerprint_size?: string;
158
+ encoding?: string;
159
+ force_flush_period?: Duration;
160
+ delete_after_read?: boolean;
161
+ /** The id of a storage extension, so offsets survive a restart. */
162
+ storage?: string;
163
+ multiline?: { line_start_pattern?: string; line_end_pattern?: string; omit_pattern?: boolean };
164
+ operators?: FileLogOperator[];
165
+ attributes?: Record<string, string>;
166
+ resource?: Record<string, string>;
167
+ retry_on_failure?: { enabled?: boolean; initial_interval?: Duration; max_interval?: Duration; max_elapsed_time?: Duration };
168
+ }
169
+
170
+ /** Tails log files and parses them with stanza operators. */
171
+ export const FileLogReceiver = defineBuiltin<FileLogReceiverConfig, "receiver", "filelog">({
172
+ kind: "receiver",
173
+ type: "filelog",
174
+ description: "Tails log files and parses each line with stanza operators",
175
+ validate: (c) => (c.include?.length ? [] : ["include is empty, so no file is read"]),
176
+ endpoints: (c) => c.include ?? [],
177
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The extension point: a component chant doesn't ship, defined by a team,
3
+ * serializes and lints exactly like a built-in, and carries its schema pin.
4
+ */
5
+ import { describe, expect, test } from "vitest";
6
+ import { load } from "js-yaml";
7
+ import type { Declarable } from "@intentius/chant/declarable";
8
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
9
+ import { otelSerializer } from "./serializer";
10
+ import { postSynthChecks } from "./lint/post-synth";
11
+ import { BatchProcessor, OtlpExporter, OtlpReceiver } from "./components";
12
+ import { COLLECTOR_PIN, defineComponent, definitionFor, isOTelComponent } from "./define";
13
+ import { Pipeline } from "./pipeline";
14
+ import { collectorTopologyOf } from "./topology";
15
+
16
+ interface VendorExporterConfig {
17
+ api: { key: string; site?: string };
18
+ queue_size?: number;
19
+ }
20
+
21
+ const VendorExporter = defineComponent<VendorExporterConfig>()({
22
+ kind: "exporter",
23
+ type: "vendor2559",
24
+ pin: { source: "@acme/otel-vendor-exporter", version: "1.4.2", digest: "sha256:0f1e2d" },
25
+ description: "Sends telemetry to Vendor",
26
+ validate: (c) => (c.api.key.includes("${") ? [] : ["api.key must be an ${env:...} reference"]),
27
+ endpoints: (c) => [`https://intake.${c.api.site ?? "vendor.example"}`],
28
+ });
29
+
30
+ /** A zod-compatible validator, without depending on zod. */
31
+ const RedactProcessor = defineComponent<{ patterns: string[] }>()({
32
+ kind: "processor",
33
+ type: "redact2559",
34
+ pin: { source: "github.com/acme/otel-redact", version: "v0.3.0" },
35
+ validate: {
36
+ safeParse: (v: unknown) =>
37
+ Array.isArray((v as { patterns?: unknown }).patterns) && (v as { patterns: unknown[] }).patterns.length > 0
38
+ ? { success: true }
39
+ : { success: false, error: { issues: [{ path: ["patterns"], message: "needs at least one pattern" }] } },
40
+ },
41
+ });
42
+
43
+ function build(list: unknown[]) {
44
+ const entities = new Map(list.map((e, i) => [`e${i}`, e as Declarable]));
45
+ const out = otelSerializer.serialize(entities);
46
+ const yaml = typeof out === "string" ? out : out.primary;
47
+ const ctx = makePostSynthCtx("otel", yaml, entities);
48
+ const diags = postSynthChecks.flatMap((c) => c.check(ctx));
49
+ return { yaml, diags, entities };
50
+ }
51
+
52
+ describe("defineComponent", () => {
53
+ test("a custom component is a component like any built-in", () => {
54
+ const v = new VendorExporter({ name: "eu", api: { key: "${env:VENDOR_KEY}", site: "vendor.eu" } });
55
+ expect(isOTelComponent(v)).toBe(true);
56
+ expect(v.componentId).toBe("vendor2559/eu");
57
+ expect(v.entityType).toBe("OTel::Exporter::vendor2559");
58
+ expect(v.lexicon).toBe("otel");
59
+ expect(definitionFor(v.entityType)?.builtin).toBe(false);
60
+ });
61
+
62
+ test("it serializes under its section with a # chant: pin header", () => {
63
+ const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
64
+ const v = new VendorExporter({ name: "eu", api: { key: "${env:VENDOR_KEY}" }, queue_size: 500 });
65
+ const { yaml, diags } = build([otlp, v, new Pipeline({ signal: "traces", receivers: [otlp], exporters: [v] })]);
66
+ expect(yaml.split("\n")[0]).toBe("# chant: exporter vendor2559/eu schema @acme/otel-vendor-exporter@1.4.2 sha256:0f1e2d");
67
+ const parsed = load(yaml) as any;
68
+ expect(parsed.exporters["vendor2559/eu"]).toEqual({ api: { key: "${env:VENDOR_KEY}" }, queue_size: 500 });
69
+ expect(parsed.service.pipelines.traces.exporters).toEqual(["vendor2559/eu"]);
70
+ expect(diags).toEqual([]);
71
+ });
72
+
73
+ test("built-ins get no header line", () => {
74
+ const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
75
+ const x = new OtlpExporter({ endpoint: "x:4317" });
76
+ const { yaml } = build([otlp, x, new Pipeline({ signal: "traces", receivers: [otlp], exporters: [x] })]);
77
+ expect(yaml.startsWith("receivers:")).toBe(true);
78
+ });
79
+
80
+ test("its own validate runs as OTEL107, function or safeParse", () => {
81
+ const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
82
+ const v = new VendorExporter({ api: { key: "plaintext" } });
83
+ const r = new RedactProcessor({ patterns: [] });
84
+ const { diags } = build([otlp, r, v, new Pipeline({ signal: "logs", receivers: [otlp], processors: [r], exporters: [v] })]);
85
+ expect(diags.map((d) => [d.checkId, d.message])).toEqual([
86
+ ["OTEL107", 'processor "redact2559": patterns: needs at least one pattern'],
87
+ ["OTEL107", 'exporter "vendor2559": api.key must be an ${env:...} reference'],
88
+ ]);
89
+ });
90
+
91
+ test("the reference and pipeline checks apply to it unchanged", () => {
92
+ const otlp = new OtlpReceiver({ protocols: { grpc: {} } });
93
+ const unused = new VendorExporter({ name: "idle", api: { key: "${env:K}" } });
94
+ const batch = new BatchProcessor({});
95
+ const { diags } = build([
96
+ otlp,
97
+ batch,
98
+ unused,
99
+ new Pipeline({ signal: "traces", receivers: [otlp], processors: [batch], exporters: ["vendor2559/missing"] }),
100
+ ]);
101
+ expect(diags.map((d) => [d.checkId, d.entity])).toEqual([
102
+ ["OTEL101", "vendor2559/missing"],
103
+ ["OTEL103", "vendor2559/idle"],
104
+ ]);
105
+ });
106
+
107
+ test("topology reports its endpoints and its pin", () => {
108
+ const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
109
+ const v = new VendorExporter({ name: "eu", api: { key: "${env:K}", site: "vendor.eu" } });
110
+ const topo = collectorTopologyOf([otlp, v, new Pipeline({ signal: "metrics", receivers: [otlp], exporters: [v] })]);
111
+ const component = topo.components.find((c) => c.id === "vendor2559/eu")!;
112
+ expect(component.builtin).toBe(false);
113
+ expect(component.schema).toEqual({ source: "@acme/otel-vendor-exporter", version: "1.4.2", digest: "sha256:0f1e2d" });
114
+ expect(topo.exporters).toEqual([
115
+ { id: "vendor2559/eu", type: "vendor2559", endpoints: ["https://intake.vendor.eu"], pipelines: ["metrics"], signals: ["metrics"] },
116
+ ]);
117
+ expect(topo.components.find((c) => c.id === "otlp")!.schema).toEqual({ ...COLLECTOR_PIN });
118
+ });
119
+
120
+ test("a built-in type cannot be redefined", () => {
121
+ expect(() =>
122
+ defineComponent<{ endpoint: string }>()({ kind: "exporter", type: "otlp", pin: { source: "x", version: "1" } }),
123
+ ).toThrow(/built in/);
124
+ });
125
+
126
+ test("a type that is not collector syntax is refused", () => {
127
+ expect(() =>
128
+ defineComponent<object>()({ kind: "exporter", type: "has-dash", pin: { source: "x", version: "1" } }),
129
+ ).toThrow(/not a collector type/);
130
+ });
131
+ });
package/src/define.ts ADDED
@@ -0,0 +1,249 @@
1
+ /**
2
+ * `defineComponent()`, the one way a collector component type enters this
3
+ * lexicon.
4
+ *
5
+ * The built-in receivers, processors, exporters and extensions are defined
6
+ * through it, and so is a component a team or plugin adds for something chant
7
+ * doesn't ship (a vendor exporter, an in-house processor). Both produce the
8
+ * same kind of class, register in the same table, serialize through the same
9
+ * code and are checked by the same post-synth checks, so there is no second
10
+ * path for a custom component to fall off.
11
+ *
12
+ * ## Schema pinning
13
+ *
14
+ * Every definition carries a `pin`: the source its config type was written
15
+ * against and the version of that source. Built-ins share `COLLECTOR_PIN`,
16
+ * which moves only when this package does, so the chant lexicon version pins
17
+ * them. A custom definition must name its own pin, typically the npm package
18
+ * (or Go module) that ships the component and the release its type follows,
19
+ * optionally with a digest of the schema document. chant records the pin; it
20
+ * does not fetch or verify the schema. The pin shows up in three places:
21
+ *
22
+ * - `collectorTopology()` reports it for every component, built-in or not.
23
+ * - The serializer writes a `# chant:` comment line per custom component at
24
+ * the top of the emitted YAML, so the file says which schema each
25
+ * non-built-in component was checked against. The collector ignores it.
26
+ * - OTEL109 fails a build whose custom component has no usable pin.
27
+ */
28
+
29
+ import { createResource } from "@intentius/chant/runtime";
30
+ import type { Declarable } from "@intentius/chant/declarable";
31
+ import { componentId, type ComponentKind } from "./model";
32
+
33
+ /** Where a component's config schema comes from, and which version of it the type follows. */
34
+ export interface SchemaPin {
35
+ /** The package, Go module or URL that defines the component's config, e.g. `@acme/otel-datadog`. */
36
+ source: string;
37
+ /** The version of `source` the TypeScript config type was written against. */
38
+ version: string;
39
+ /** Optional digest of the schema document itself, e.g. `sha256:…`. Recorded as given. */
40
+ digest?: string;
41
+ }
42
+
43
+ /**
44
+ * The collector distribution the built-in config types follow. Bumping it is a
45
+ * lexicon change like any other, released with this package.
46
+ */
47
+ export const COLLECTOR_PIN: SchemaPin = Object.freeze({
48
+ source: "github.com/open-telemetry/opentelemetry-collector-contrib",
49
+ version: "v0.130.0",
50
+ });
51
+
52
+ /** A zod-compatible schema: anything with `safeParse`. Keeps zod optional. */
53
+ export interface SafeParseSchema {
54
+ safeParse(value: unknown): {
55
+ success: boolean;
56
+ error?: { issues?: Array<{ path?: ReadonlyArray<PropertyKey>; message: string }>; message?: string };
57
+ };
58
+ }
59
+
60
+ /** A config check: a function returning problems, or a zod-compatible schema. */
61
+ export type ConfigValidator<C> = ((config: C) => string[]) | SafeParseSchema;
62
+
63
+ export interface ComponentDefinition<K extends ComponentKind = ComponentKind, T extends string = string, C = Record<string, unknown>> {
64
+ kind: K;
65
+ /** The collector type, the part of the id before `/`. */
66
+ type: T;
67
+ /** Where the config schema comes from. Required for a custom component. */
68
+ pin: SchemaPin;
69
+ /** True only for the components this package ships. */
70
+ builtin: boolean;
71
+ /** One line for hover and docs. */
72
+ description?: string;
73
+ /** Extra checks on the config beyond what its TypeScript type enforces (OTEL107). */
74
+ validate?: ConfigValidator<C>;
75
+ /** Where this component sends or listens, for `collectorTopology()`. */
76
+ endpoints?: (config: C) => string[];
77
+ }
78
+
79
+ /** What a custom component supplies. `builtin` is always false for these. */
80
+ export type CustomComponentOptions<K extends ComponentKind, T extends string, C> = Omit<
81
+ ComponentDefinition<K, T, C>,
82
+ "builtin"
83
+ >;
84
+
85
+ /** Constructor props: the component's own config plus the optional instance `name`. */
86
+ export type ComponentProps<C> = C & {
87
+ /** The instance name. The component id becomes `type/name`; without it the id is just `type`. */
88
+ name?: string;
89
+ };
90
+
91
+ /** A declared collector component. */
92
+ export interface OTelComponent<K extends ComponentKind = ComponentKind, T extends string = string, C = Record<string, unknown>>
93
+ extends Declarable {
94
+ readonly props: ComponentProps<C>;
95
+ readonly componentKind: K;
96
+ readonly componentType: T;
97
+ /** The collector id, `type` or `type/name`. */
98
+ readonly componentId: string;
99
+ }
100
+
101
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
102
+ type CtorArgs<C> = {} extends C ? [props?: ComponentProps<C>] : [props: ComponentProps<C>];
103
+
104
+ export interface ComponentClass<K extends ComponentKind = ComponentKind, T extends string = string, C = Record<string, unknown>> {
105
+ new (...args: CtorArgs<C>): OTelComponent<K, T, C>;
106
+ readonly definition: ComponentDefinition<K, T, C>;
107
+ }
108
+
109
+ const KIND_SEGMENT: Record<ComponentKind, string> = {
110
+ receiver: "Receiver",
111
+ processor: "Processor",
112
+ exporter: "Exporter",
113
+ extension: "Extension",
114
+ };
115
+
116
+ /** The chant entity type for a component, e.g. `OTel::Exporter::otlp`. */
117
+ export function componentEntityType(kind: ComponentKind, type: string): string {
118
+ return `OTel::${KIND_SEGMENT[kind]}::${type}`;
119
+ }
120
+
121
+ // One table per process, keyed by entity type. Held on globalThis so a second
122
+ // copy of this module (a test runner and a built dist, say) shares it.
123
+ const REGISTRY_KEY = Symbol.for("chant.otel.componentDefinitions");
124
+ function registry(): Map<string, ComponentDefinition> {
125
+ const g = globalThis as unknown as Record<symbol, Map<string, ComponentDefinition> | undefined>;
126
+ let table = g[REGISTRY_KEY];
127
+ if (!table) {
128
+ table = new Map();
129
+ g[REGISTRY_KEY] = table;
130
+ }
131
+ return table;
132
+ }
133
+
134
+ /** The definition behind an entity type, if one has been registered in this process. */
135
+ export function definitionFor(entityType: string): ComponentDefinition | undefined {
136
+ return registry().get(entityType);
137
+ }
138
+
139
+ /** The definition for a collector `kind` + `type`, e.g. the built-in `exporter` `otlp`. */
140
+ export function definitionOf(kind: ComponentKind, type: string): ComponentDefinition | undefined {
141
+ return registry().get(componentEntityType(kind, type));
142
+ }
143
+
144
+ /** Every registered definition, built-ins first in registration order. */
145
+ export function registeredDefinitions(): ComponentDefinition[] {
146
+ return [...registry().values()];
147
+ }
148
+
149
+ const TYPE_PATTERN = /^[A-Za-z0-9_]+$/;
150
+
151
+ function makeClass<K extends ComponentKind, T extends string, C>(
152
+ def: ComponentDefinition<K, T, C>,
153
+ ): ComponentClass<K, T, C> {
154
+ if (!TYPE_PATTERN.test(def.type)) {
155
+ throw new Error(`otel: component type "${def.type}" is not a collector type (letters, digits and _ only)`);
156
+ }
157
+ const entityType = componentEntityType(def.kind, def.type);
158
+ const existing = registry().get(entityType);
159
+ if (existing?.builtin && !def.builtin) {
160
+ throw new Error(
161
+ `otel: ${def.kind} "${def.type}" is built in. Declare an instance with a name (${def.type}/<name>) instead of defining it again.`,
162
+ );
163
+ }
164
+ registry().set(entityType, def as unknown as ComponentDefinition);
165
+
166
+ const Base = createResource(entityType, "otel", {}) as unknown as (this: object, props: Record<string, unknown>) => void;
167
+ const Cls = function (this: object, props?: Record<string, unknown>) {
168
+ const p = props ?? {};
169
+ Base.call(this, p);
170
+ const name = typeof p.name === "string" ? p.name : undefined;
171
+ Object.defineProperty(this, "componentKind", { value: def.kind, enumerable: false });
172
+ Object.defineProperty(this, "componentType", { value: def.type, enumerable: false });
173
+ Object.defineProperty(this, "componentId", { value: componentId(def.type, name), enumerable: false });
174
+ };
175
+ Object.defineProperty(Cls, "name", { value: `${def.type}${KIND_SEGMENT[def.kind]}` });
176
+ Object.defineProperty(Cls, "definition", { value: def, enumerable: false });
177
+ return Cls as unknown as ComponentClass<K, T, C>;
178
+ }
179
+
180
+ /** Internal: how the built-ins in `./components` are defined. */
181
+ export function defineBuiltin<C, K extends ComponentKind = ComponentKind, T extends string = string>(
182
+ def: Omit<ComponentDefinition<K, T, C>, "builtin" | "pin">,
183
+ ): ComponentClass<K, T, C> {
184
+ return makeClass<K, T, C>({ ...def, pin: COLLECTOR_PIN, builtin: true });
185
+ }
186
+
187
+ /**
188
+ * Define a collector component chant doesn't ship.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * interface DatadogExporterConfig {
193
+ * api: { key: string; site?: string };
194
+ * traces?: { span_name_as_resource_name?: boolean };
195
+ * }
196
+ *
197
+ * export const DatadogExporter = defineComponent<DatadogExporterConfig>()({
198
+ * kind: "exporter",
199
+ * type: "datadog",
200
+ * pin: { source: "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter", version: "v0.130.0" },
201
+ * validate: (c) => (c.api.key.startsWith("${env:") ? [] : ["api.key should come from ${env:...}"]),
202
+ * endpoints: (c) => [`https://api.${c.api.site ?? "datadoghq.com"}`],
203
+ * });
204
+ *
205
+ * export const dd = new DatadogExporter({ api: { key: "${env:DD_API_KEY}" } });
206
+ * ```
207
+ *
208
+ * The curried form lets you name the config type while `kind` and `type` are
209
+ * still inferred from the literal you pass.
210
+ */
211
+ export function defineComponent<C>() {
212
+ return function <K extends ComponentKind, T extends string>(
213
+ options: CustomComponentOptions<K, T, C>,
214
+ ): ComponentClass<K, T, C> {
215
+ return makeClass<K, T, C>({ ...options, builtin: false });
216
+ };
217
+ }
218
+
219
+ /** True when `value` is a declared collector component (built-in or custom). */
220
+ export function isOTelComponent(value: unknown): value is OTelComponent {
221
+ return (
222
+ typeof value === "object" &&
223
+ value !== null &&
224
+ typeof (value as { componentKind?: unknown }).componentKind === "string" &&
225
+ typeof (value as { componentId?: unknown }).componentId === "string" &&
226
+ (value as { lexicon?: unknown }).lexicon === "otel"
227
+ );
228
+ }
229
+
230
+ /** Run a definition's validator, normalising both validator forms to a list of messages. */
231
+ export function runValidator<C>(validator: ConfigValidator<C> | undefined, config: C): string[] {
232
+ if (!validator) return [];
233
+ if (typeof validator === "function") return validator(config);
234
+ const result = validator.safeParse(config);
235
+ if (result.success) return [];
236
+ const issues = result.error?.issues ?? [];
237
+ if (issues.length === 0) return [result.error?.message ?? "config does not match its schema"];
238
+ return issues.map((i) => {
239
+ const path = (i.path ?? []).map(String).join(".");
240
+ return path ? `${path}: ${i.message}` : i.message;
241
+ });
242
+ }
243
+
244
+ /** A pin is usable when it names a source and a version. */
245
+ export function isUsablePin(pin: unknown): pin is SchemaPin {
246
+ if (typeof pin !== "object" || pin === null) return false;
247
+ const p = pin as Record<string, unknown>;
248
+ return typeof p.source === "string" && p.source.trim() !== "" && typeof p.version === "string" && p.version.trim() !== "";
249
+ }
package/src/detect.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Template detection for the otel lexicon: a parsed document is a collector
3
+ * config when it has `service.pipelines` and a receivers or exporters
4
+ * section. Kept free of the plugin and the TypeScript compiler so it bundles
5
+ * for edge runtimes, like the other lexicons' `detect` modules.
6
+ */
7
+ import { looksLikeCollectorConfig } from "./model";
8
+
9
+ export function detectTemplate(data: unknown): boolean {
10
+ return looksLikeCollectorConfig(data);
11
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "AttributesProcessor": {
3
+ "resourceType": "OTel::Processor::attributes",
4
+ "kind": "resource",
5
+ "lexicon": "otel"
6
+ },
7
+ "BatchProcessor": {
8
+ "resourceType": "OTel::Processor::batch",
9
+ "kind": "resource",
10
+ "lexicon": "otel"
11
+ },
12
+ "DebugExporter": {
13
+ "resourceType": "OTel::Exporter::debug",
14
+ "kind": "resource",
15
+ "lexicon": "otel"
16
+ },
17
+ "FileLogReceiver": {
18
+ "resourceType": "OTel::Receiver::filelog",
19
+ "kind": "resource",
20
+ "lexicon": "otel"
21
+ },
22
+ "GoogleCloudExporter": {
23
+ "resourceType": "OTel::Exporter::googlecloud",
24
+ "kind": "resource",
25
+ "lexicon": "otel"
26
+ },
27
+ "HealthCheckExtension": {
28
+ "resourceType": "OTel::Extension::health_check",
29
+ "kind": "resource",
30
+ "lexicon": "otel"
31
+ },
32
+ "HostMetricsReceiver": {
33
+ "resourceType": "OTel::Receiver::hostmetrics",
34
+ "kind": "resource",
35
+ "lexicon": "otel"
36
+ },
37
+ "K8sAttributesProcessor": {
38
+ "resourceType": "OTel::Processor::k8sattributes",
39
+ "kind": "resource",
40
+ "lexicon": "otel"
41
+ },
42
+ "MemoryLimiterProcessor": {
43
+ "resourceType": "OTel::Processor::memory_limiter",
44
+ "kind": "resource",
45
+ "lexicon": "otel"
46
+ },
47
+ "OtlpExporter": {
48
+ "resourceType": "OTel::Exporter::otlp",
49
+ "kind": "resource",
50
+ "lexicon": "otel"
51
+ },
52
+ "OtlpHttpExporter": {
53
+ "resourceType": "OTel::Exporter::otlphttp",
54
+ "kind": "resource",
55
+ "lexicon": "otel"
56
+ },
57
+ "OtlpReceiver": {
58
+ "resourceType": "OTel::Receiver::otlp",
59
+ "kind": "resource",
60
+ "lexicon": "otel"
61
+ },
62
+ "Pipeline": {
63
+ "resourceType": "OTel::Pipeline",
64
+ "kind": "resource",
65
+ "lexicon": "otel"
66
+ },
67
+ "PprofExtension": {
68
+ "resourceType": "OTel::Extension::pprof",
69
+ "kind": "resource",
70
+ "lexicon": "otel"
71
+ },
72
+ "PrometheusExporter": {
73
+ "resourceType": "OTel::Exporter::prometheus",
74
+ "kind": "resource",
75
+ "lexicon": "otel"
76
+ },
77
+ "PrometheusReceiver": {
78
+ "resourceType": "OTel::Receiver::prometheus",
79
+ "kind": "resource",
80
+ "lexicon": "otel"
81
+ },
82
+ "ResourceDetectionProcessor": {
83
+ "resourceType": "OTel::Processor::resourcedetection",
84
+ "kind": "resource",
85
+ "lexicon": "otel"
86
+ },
87
+ "ResourceProcessor": {
88
+ "resourceType": "OTel::Processor::resource",
89
+ "kind": "resource",
90
+ "lexicon": "otel"
91
+ },
92
+ "Service": {
93
+ "resourceType": "OTel::Service",
94
+ "kind": "resource",
95
+ "lexicon": "otel"
96
+ },
97
+ "ZPagesExtension": {
98
+ "resourceType": "OTel::Extension::zpages",
99
+ "kind": "resource",
100
+ "lexicon": "otel"
101
+ }
102
+ }