@intentius/chant-lexicon-otel 0.97.0 → 0.99.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 (54) hide show
  1. package/README.md +5 -0
  2. package/dist/codegen/docs.d.ts.map +1 -1
  3. package/dist/components/common.d.ts +7 -2
  4. package/dist/components/common.d.ts.map +1 -1
  5. package/dist/components/filtering.d.ts +58 -1
  6. package/dist/components/filtering.d.ts.map +1 -1
  7. package/dist/components/processors.d.ts +10 -0
  8. package/dist/components/processors.d.ts.map +1 -1
  9. package/dist/components/receivers.d.ts +87 -7
  10. package/dist/components/receivers.d.ts.map +1 -1
  11. package/dist/import/embedded.d.ts +12 -0
  12. package/dist/import/embedded.d.ts.map +1 -0
  13. package/dist/import/generator.d.ts +39 -0
  14. package/dist/import/generator.d.ts.map +1 -0
  15. package/dist/import/parser.d.ts +61 -0
  16. package/dist/import/parser.d.ts.map +1 -0
  17. package/dist/integrity.json +4 -4
  18. package/dist/lint/rules/literal-credential.d.ts.map +1 -1
  19. package/dist/manifest.json +1 -1
  20. package/dist/plugin.d.ts.map +1 -1
  21. package/dist/rules/literal-credential.ts +46 -17
  22. package/dist/skills/chant-otel.md +11 -0
  23. package/package.json +2 -2
  24. package/src/codegen/docs.ts +2 -1
  25. package/src/components/common.ts +7 -2
  26. package/src/components/filtering.test.ts +27 -0
  27. package/src/components/filtering.ts +65 -2
  28. package/src/components/processors.ts +4 -0
  29. package/src/components/receivers.ts +88 -4
  30. package/src/import/embedded.test.ts +56 -0
  31. package/src/import/embedded.ts +41 -0
  32. package/src/import/generated-types.e2e.test.ts +110 -0
  33. package/src/import/generator.test.ts +165 -0
  34. package/src/import/generator.ts +509 -0
  35. package/src/import/parser.test.ts +174 -0
  36. package/src/import/parser.ts +236 -0
  37. package/src/import/roundtrip.test.ts +365 -0
  38. package/src/import/testdata/agent-observability-agent.yaml +54 -0
  39. package/src/import/testdata/agent-observability-gateway.yaml +128 -0
  40. package/src/import/testdata/fixtures.ts +274 -0
  41. package/src/import/testdata/gateway.yaml +153 -0
  42. package/src/import/testdata/upstream/couchbase.yaml +150 -0
  43. package/src/import/testdata/upstream/fault-tolerant-logs.yaml +27 -0
  44. package/src/import/testdata/upstream/kubernetes-filelog.yaml +86 -0
  45. package/src/import/testdata/upstream/loadbalancing-agent.yaml +38 -0
  46. package/src/import/testdata/upstream/loadbalancing-backend.yaml +27 -0
  47. package/src/import/testdata/upstream/logline-filter-in.yaml +20 -0
  48. package/src/import/testdata/upstream/logline-filter-out.yaml +20 -0
  49. package/src/import/testdata/upstream/secure-tracing.yaml +26 -0
  50. package/src/import/testdata/upstream/servicegraph-nop.yaml +28 -0
  51. package/src/lint/rules/literal-credential.ts +46 -17
  52. package/src/lint/rules/rules.test.ts +16 -0
  53. package/src/plugin.ts +15 -0
  54. package/src/skills/chant-otel.md +11 -0
@@ -44,7 +44,8 @@ RED and token metrics.
44
44
  Their config types follow the collector-contrib release named on the
45
45
  Custom components page. A component chant doesn't ship is added with
46
46
  \`defineComponent\`, and it is serialized and checked the same way as the
47
- built-ins.
47
+ built-ins. \`chant import\` turns an existing collector config file into this
48
+ TypeScript (see Importing a Collector Config).
48
49
 
49
50
  Checks catch a pipeline that uses a component nobody declared (OTEL101), a
50
51
  pipeline with no receivers or exporters (OTEL102), a declared component no
@@ -5,8 +5,13 @@
5
5
  * unchanged.
6
6
  */
7
7
 
8
- /** A duration string the collector parses, e.g. `5s`, `250ms`, `1m30s`. */
9
- export type Duration = string;
8
+ /**
9
+ * A duration the collector parses: a string such as `5s`, `250ms` or
10
+ * `1m30s`, or a bare integer, which the collector reads as nanoseconds
11
+ * (collector-contrib's own servicegraph test config writes
12
+ * `latency_histogram_buckets: [1,2,3,4,5]`). Prefer the string form.
13
+ */
14
+ export type Duration = string | number;
10
15
 
11
16
  export interface TLSClientSettings {
12
17
  insecure?: boolean;
@@ -81,6 +81,33 @@ describe("filter processor", () => {
81
81
  );
82
82
  expect(problems(new FilterProcessor({ logs: { log_record: ["true"] } }))).toEqual([]);
83
83
  });
84
+
85
+ test("the legacy include/exclude match blocks serialize as written and count as conditions", () => {
86
+ const filter = new FilterProcessor({
87
+ metrics: { exclude: { match_type: "strict", metric_names: ["up", "scrape_duration_seconds"] } },
88
+ logs: { include: { match_type: "regexp", severity_number: { min: "WARN", match_undefined: true }, bodies: ["^panic"] } },
89
+ spans: { exclude: { match_type: "strict", services: ["healthcheck"], span_kinds: ["SPAN_KIND_INTERNAL"] } },
90
+ });
91
+ expect(problems(filter)).toEqual([]);
92
+ const parsed = load(primary(otelSerializer.serialize(entities({ filter })))) as any;
93
+ expect(parsed.processors.filter.metrics.exclude.metric_names).toEqual(["up", "scrape_duration_seconds"]);
94
+ expect(parsed.processors.filter.logs.include.severity_number).toEqual({ min: "WARN", match_undefined: true });
95
+ expect(parsed.processors.filter.spans.exclude.span_kinds).toEqual(["SPAN_KIND_INTERNAL"]);
96
+ });
97
+
98
+ test("a signal that mixes OTTL conditions with include/exclude is reported, as the collector rejects it", () => {
99
+ const mixed = new FilterProcessor({
100
+ traces: { span: ["true"] },
101
+ spans: { include: { match_type: "strict", services: ["a"] } },
102
+ metrics: { metric: ["true"], exclude: { match_type: "expr", expressions: ["MetricName == 'up'"] } },
103
+ logs: { log_record: ["true"], exclude: { match_type: "strict", bodies: ["x"] } },
104
+ });
105
+ expect(problems(mixed)).toEqual([
106
+ "traces mixes OTTL conditions with include/exclude, which the collector rejects",
107
+ "metrics mixes OTTL conditions with include/exclude, which the collector rejects",
108
+ "logs mixes OTTL conditions with include/exclude, which the collector rejects",
109
+ ]);
110
+ });
84
111
  });
85
112
 
86
113
  describe("transform processor", () => {
@@ -8,13 +8,56 @@
8
8
  */
9
9
 
10
10
  import { defineBuiltin } from "../define";
11
+ import type { AttributesMatch } from "./processors";
11
12
 
12
13
  /** What an OTTL processor does when a condition or statement errors at run time. */
13
14
  export type OttlErrorMode = "ignore" | "silent" | "propagate";
14
15
 
15
16
  // ── filter ───────────────────────────────────────────────────────────
16
17
 
17
- /** OTTL conditions per context. Telemetry matching any condition is dropped. */
18
+ /** An attribute a legacy include/exclude block matches on (`filterconfig.Attribute`). */
19
+ export interface FilterAttributeMatch {
20
+ key: string;
21
+ value?: unknown;
22
+ }
23
+
24
+ /** Options for the `regexp` match type (`filterset/regexp.Config`). */
25
+ export interface FilterRegexpSettings {
26
+ cacheenabled?: boolean;
27
+ cachemaxnumentries?: number;
28
+ }
29
+
30
+ /**
31
+ * The filter processor's legacy `metrics.include` / `metrics.exclude` block
32
+ * (`filterconfig.MetricMatchProperties`). `expr` takes `expressions` instead
33
+ * of `metric_names`.
34
+ */
35
+ export interface FilterMetricMatch {
36
+ match_type: "strict" | "regexp" | "expr";
37
+ regexp?: FilterRegexpSettings;
38
+ metric_names?: string[];
39
+ expressions?: string[];
40
+ resource_attributes?: FilterAttributeMatch[];
41
+ }
42
+
43
+ /** The filter processor's legacy `logs.include` / `logs.exclude` block (`LogMatchProperties`). */
44
+ export interface FilterLogMatch {
45
+ match_type: "strict" | "regexp";
46
+ resource_attributes?: FilterAttributeMatch[];
47
+ record_attributes?: FilterAttributeMatch[];
48
+ severity_texts?: string[];
49
+ /** Matches records at or above `min`, a severity number or name such as `INFO`. */
50
+ severity_number?: { min: number | string; match_undefined?: boolean };
51
+ bodies?: string[];
52
+ }
53
+
54
+ /**
55
+ * Conditions per signal. Telemetry matching any condition is dropped.
56
+ *
57
+ * Each signal takes OTTL conditions or the legacy `include` / `exclude`
58
+ * match blocks (for traces, the legacy blocks sit under `spans`), not both:
59
+ * the collector rejects a signal that mixes them.
60
+ */
18
61
  export interface FilterProcessorConfig {
19
62
  /** Default `propagate`: an erroring condition fails the batch. `ignore` logs and moves on. */
20
63
  error_mode?: OttlErrorMode;
@@ -24,15 +67,27 @@ export interface FilterProcessorConfig {
24
67
  /** Drops the span event. */
25
68
  spanevent?: string[];
26
69
  };
70
+ /** Legacy span matching. With `include`, spans that do not match are dropped; with `exclude`, spans that match. */
71
+ spans?: { include?: AttributesMatch; exclude?: AttributesMatch };
27
72
  metrics?: {
28
73
  /** Drops the whole metric, e.g. `name == "http.server.duration" and type == METRIC_DATA_TYPE_HISTOGRAM`. */
29
74
  metric?: string[];
30
75
  /** Drops the data point. */
31
76
  datapoint?: string[];
77
+ /** Legacy: keeps only the metrics that match. */
78
+ include?: FilterMetricMatch;
79
+ /** Legacy: drops the metrics that match. */
80
+ exclude?: FilterMetricMatch;
81
+ /** Legacy: options for the `regexp` match type. */
82
+ regexp?: FilterRegexpSettings;
32
83
  };
33
84
  logs?: {
34
85
  /** Drops the log record, e.g. `severity_number < SEVERITY_NUMBER_WARN`. */
35
86
  log_record?: string[];
87
+ /** Legacy: keeps only the log records that match. */
88
+ include?: FilterLogMatch;
89
+ /** Legacy: drops the log records that match. */
90
+ exclude?: FilterLogMatch;
36
91
  };
37
92
  }
38
93
 
@@ -55,7 +110,15 @@ export const FilterProcessor = defineBuiltin<FilterProcessorConfig, "processor",
55
110
  if (typeof cond !== "string" || cond.trim() === "") problems.push(`${path}[${i}] is an empty condition`);
56
111
  });
57
112
  }
58
- if (lists.every(([, list]) => !list || list.length === 0)) {
113
+ const legacy: Array<[string, boolean, boolean]> = [
114
+ ["traces", !!(c.traces?.span || c.traces?.spanevent), !!(c.spans?.include || c.spans?.exclude)],
115
+ ["metrics", !!(c.metrics?.metric || c.metrics?.datapoint), !!(c.metrics?.include || c.metrics?.exclude)],
116
+ ["logs", !!c.logs?.log_record, !!(c.logs?.include || c.logs?.exclude)],
117
+ ];
118
+ for (const [signal, ottl, matchBlocks] of legacy) {
119
+ if (ottl && matchBlocks) problems.push(`${signal} mixes OTTL conditions with include/exclude, which the collector rejects`);
120
+ }
121
+ if (lists.every(([, list]) => !list || list.length === 0) && legacy.every(([, , matchBlocks]) => !matchBlocks)) {
59
122
  problems.push("no condition is set for any signal, so the processor drops nothing");
60
123
  }
61
124
  return problems;
@@ -73,12 +73,16 @@ export const ResourceProcessor = defineBuiltin<ResourceProcessorConfig, "process
73
73
 
74
74
  // ── attributes ───────────────────────────────────────────────────────
75
75
 
76
+ /** An include/exclude block of the attributes processor and the filter processor's legacy `spans` (`filterconfig.MatchProperties`). */
76
77
  export interface AttributesMatch {
77
78
  match_type: "strict" | "regexp";
79
+ regexp?: { cacheenabled?: boolean; cachemaxnumentries?: number };
78
80
  services?: string[];
79
81
  span_names?: string[];
82
+ span_kinds?: string[];
80
83
  log_bodies?: string[];
81
84
  log_severity_texts?: string[];
85
+ log_severity_number?: { min: number | string; match_undefined?: boolean };
82
86
  metric_names?: string[];
83
87
  attributes?: Array<{ key: string; value?: unknown }>;
84
88
  resources?: Array<{ key: string; value?: unknown }>;
@@ -44,25 +44,109 @@ export interface PrometheusStaticConfig {
44
44
  labels?: Record<string, string>;
45
45
  }
46
46
 
47
- /** One Prometheus scrape job. Service-discovery and relabel blocks pass through as written. */
48
- export interface PrometheusScrapeConfig {
47
+ /** Prometheus's `tls_config` (prometheus/common `TLSConfig`). Versions are spelled `TLS12`, `TLS13`. */
48
+ export interface PrometheusTLSConfig {
49
+ ca?: string;
50
+ cert?: string;
51
+ key?: string;
52
+ ca_file?: string;
53
+ cert_file?: string;
54
+ key_file?: string;
55
+ ca_ref?: string;
56
+ cert_ref?: string;
57
+ key_ref?: string;
58
+ server_name?: string;
59
+ insecure_skip_verify?: boolean;
60
+ min_version?: string;
61
+ max_version?: string;
62
+ }
63
+
64
+ /** Prometheus's proxy settings, inlined into the HTTP client and OAuth2 blocks. */
65
+ export interface PrometheusProxySettings {
66
+ proxy_url?: string;
67
+ no_proxy?: string;
68
+ proxy_from_environment?: boolean;
69
+ proxy_connect_header?: Record<string, string[]>;
70
+ }
71
+
72
+ /** Scrape authentication with a user name and password. Prefer `password_file` or `${env:...}` to a literal password. */
73
+ export interface PrometheusBasicAuth {
74
+ username?: string;
75
+ username_file?: string;
76
+ username_ref?: string;
77
+ password?: string;
78
+ password_file?: string;
79
+ password_ref?: string;
80
+ }
81
+
82
+ /** An `Authorization` header: `type` defaults to `Bearer`. */
83
+ export interface PrometheusAuthorization {
84
+ type?: string;
85
+ credentials?: string;
86
+ credentials_file?: string;
87
+ credentials_ref?: string;
88
+ }
89
+
90
+ export interface PrometheusOAuth2 extends PrometheusProxySettings {
91
+ client_id: string;
92
+ client_secret?: string;
93
+ client_secret_file?: string;
94
+ client_secret_ref?: string;
95
+ scopes?: string[];
96
+ token_url: string;
97
+ endpoint_params?: Record<string, string>;
98
+ tls_config?: PrometheusTLSConfig;
99
+ }
100
+
101
+ /**
102
+ * One Prometheus scrape job, as the Prometheus release collector-contrib
103
+ * v0.130.0 embeds reads it (scrape settings plus prometheus/common's HTTP
104
+ * client settings). Service-discovery (`*_sd_configs`) and relabel blocks
105
+ * pass through as written.
106
+ */
107
+ export interface PrometheusScrapeConfig extends PrometheusProxySettings {
49
108
  job_name: string;
50
109
  scrape_interval?: Duration;
51
110
  scrape_timeout?: Duration;
111
+ scrape_protocols?: string[];
112
+ fallback_scrape_protocol?: string;
113
+ always_scrape_classic_histograms?: boolean;
114
+ convert_classic_histograms_to_nhcb?: boolean;
115
+ scrape_failure_log_file?: string;
52
116
  metrics_path?: string;
53
117
  scheme?: "http" | "https";
54
118
  honor_labels?: boolean;
55
119
  honor_timestamps?: boolean;
120
+ track_timestamps_staleness?: boolean;
121
+ enable_compression?: boolean;
56
122
  params?: Record<string, string[]>;
57
123
  static_configs?: PrometheusStaticConfig[];
58
124
  kubernetes_sd_configs?: Array<Record<string, unknown>>;
59
125
  file_sd_configs?: Array<Record<string, unknown>>;
126
+ /** Any other service-discovery block (`dns_sd_configs`, `ec2_sd_configs`, `consul_sd_configs`, ...). */
127
+ [sd: `${string}_sd_configs`]: Array<Record<string, unknown>> | undefined;
60
128
  relabel_configs?: Array<Record<string, unknown>>;
61
129
  metric_relabel_configs?: Array<Record<string, unknown>>;
62
- tls_config?: Record<string, unknown>;
63
- authorization?: { type?: string; credentials_file?: string };
130
+ basic_auth?: PrometheusBasicAuth;
131
+ authorization?: PrometheusAuthorization;
132
+ oauth2?: PrometheusOAuth2;
133
+ bearer_token?: string;
64
134
  bearer_token_file?: string;
135
+ tls_config?: PrometheusTLSConfig;
136
+ follow_redirects?: boolean;
137
+ enable_http2?: boolean;
138
+ http_headers?: Record<string, { values?: string[]; secrets?: string[]; files?: string[] }>;
139
+ body_size_limit?: string;
65
140
  sample_limit?: number;
141
+ target_limit?: number;
142
+ label_limit?: number;
143
+ label_name_length_limit?: number;
144
+ label_value_length_limit?: number;
145
+ native_histogram_bucket_limit?: number;
146
+ native_histogram_min_bucket_factor?: number;
147
+ keep_dropped_targets?: number;
148
+ metric_name_validation_scheme?: "utf8" | "legacy";
149
+ metric_name_escaping_scheme?: "allow-utf-8" | "underscores" | "dots" | "values";
66
150
  }
67
151
 
68
152
  export interface PrometheusReceiverConfig {
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { embeddedDocument, type EmbeddedContent } from "@intentius/chant/import/embedded";
3
+ import { otelPlugin } from "../plugin";
4
+ import { collectorConfigImporter } from "./embedded";
5
+
6
+ const CONFIG = `receivers:
7
+ otlp:
8
+ protocols:
9
+ grpc: {}
10
+ mycorp: {}
11
+ exporters:
12
+ debug: {}
13
+ service:
14
+ pipelines:
15
+ traces:
16
+ receivers: [otlp, mycorp]
17
+ exporters: [debug]
18
+ `;
19
+
20
+ const site = (text: string, over: Partial<EmbeddedContent> = {}): EmbeddedContent => ({
21
+ host: "k8s",
22
+ hostType: "K8s::Core::ConfigMap",
23
+ location: 'ConfigMap agent data["config.yaml"]',
24
+ directory: "agent",
25
+ text,
26
+ document: embeddedDocument(text),
27
+ ...over,
28
+ });
29
+
30
+ describe("a collector config embedded in another lexicon's resource (#2962)", () => {
31
+ test("the plugin registers the importer", () => {
32
+ expect(otelPlugin.embeddedImporters?.()).toEqual([collectorConfigImporter]);
33
+ });
34
+
35
+ test("matches collector configs held as text, nothing else", () => {
36
+ expect(collectorConfigImporter.matches(site(CONFIG))).toBe(true);
37
+ expect(collectorConfigImporter.matches(site("groups: []\n"))).toBe(false);
38
+ expect(collectorConfigImporter.matches(site(CONFIG, { select: "config" }))).toBe(false);
39
+ });
40
+
41
+ test("imports the config as `chant import` does, referenced through collectorYaml", () => {
42
+ const out = collectorConfigImporter.import(site(CONFIG));
43
+ expect(out.files.map((f) => f.path)).toEqual(["custom-components.ts", "receivers.ts", "exporters.ts", "pipelines.ts"]);
44
+ // Every declared component and pipeline, not the custom component's class.
45
+ expect(out.value).toEqual({
46
+ bindings: [
47
+ { from: "receivers.ts", name: "otlp" },
48
+ { from: "receivers.ts", name: "mycorp" },
49
+ { from: "exporters.ts", name: "debug" },
50
+ { from: "pipelines.ts", name: "traces" },
51
+ ],
52
+ shape: "list",
53
+ through: { from: "@intentius/chant-lexicon-otel", name: "collectorYaml" },
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A collector config embedded in another lexicon's resource, e.g. the
3
+ * `config.yaml` of a k8s ConfigMap, for `chant import` (#2962).
4
+ *
5
+ * The config is imported exactly as `chant import config.yaml` would import
6
+ * it, into a directory of its own, and the host's value becomes
7
+ * `collectorYaml([...])` over every component, pipeline and service the
8
+ * import declares, which renders the same config text at build time.
9
+ */
10
+
11
+ import type { EmbeddedContentImporter, EmbeddedImport } from "@intentius/chant/import/embedded";
12
+ import { exportedNames } from "@intentius/chant/import/embedded";
13
+ import { looksLikeCollectorConfig } from "../model";
14
+ import { OtelCollectorParser } from "./parser";
15
+ import { OtelCollectorGenerator } from "./generator";
16
+
17
+ const PACKAGE = "@intentius/chant-lexicon-otel";
18
+
19
+ /** The module holding `defineComponent` classes: types, not config entities. */
20
+ const CUSTOM_COMPONENTS = "custom-components.ts";
21
+
22
+ export const collectorConfigImporter: EmbeddedContentImporter = {
23
+ what: "an OpenTelemetry Collector config",
24
+
25
+ matches(content) {
26
+ return typeof content.text === "string" && content.select === undefined && looksLikeCollectorConfig(content.document);
27
+ },
28
+
29
+ import(content): EmbeddedImport {
30
+ const ir = new OtelCollectorParser().parse(content.text!);
31
+ const files = new OtelCollectorGenerator().generate(ir);
32
+ const bindings = files
33
+ .filter((f) => f.path !== CUSTOM_COMPONENTS)
34
+ .flatMap((f) => exportedNames(f.content).map((name) => ({ from: f.path, name })));
35
+ return {
36
+ files,
37
+ value: { bindings, shape: "list", through: { from: PACKAGE, name: "collectorYaml" } },
38
+ warnings: ir.warnings ?? [],
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The generated source type-checks against the lexicon's config types.
3
+ * Compiling a dozen generated projects takes longer than the unit-test
4
+ * budget allows, so this runs with the e2e tests.
5
+ */
6
+ import { describe, expect, test } from "vitest";
7
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "fs";
8
+ import { join } from "path";
9
+ import * as ts from "typescript";
10
+ import { collectorYaml } from "../collector";
11
+ import { genAiPipeline } from "../genai";
12
+ import { OtelCollectorParser } from "./parser";
13
+ import { OtelCollectorGenerator } from "./generator";
14
+ import { everyBuiltin, exampleOutputs, pkgDir, read, repoRoot } from "./testdata/fixtures";
15
+
16
+ /** Type errors in generated projects, compiled together against the lexicon's source with the repo's compiler options. */
17
+ function typeErrors(projects: Record<string, Array<{ path: string; content: string }>>): string[] {
18
+ const dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
19
+ try {
20
+ const files: string[] = [];
21
+ for (const [name, generated] of Object.entries(projects)) {
22
+ const sub = join(dir, name.replace(/[^A-Za-z0-9]+/g, "-"));
23
+ mkdirSync(sub);
24
+ for (const f of generated) {
25
+ writeFileSync(join(sub, f.path), f.content);
26
+ files.push(join(sub, f.path));
27
+ }
28
+ }
29
+ const configFile = ts.readConfigFile(join(repoRoot, "tsconfig.json"), ts.sys.readFile);
30
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, repoRoot);
31
+ const program = ts.createProgram(files, { ...parsed.options, noEmit: true });
32
+ const real = (f: string) => ts.sys.realpath?.(f) ?? f;
33
+ const fileSet = new Set(files.map(real));
34
+ const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => d.file && fileSet.has(real(d.file.fileName)));
35
+ // A program that saw none of the files would pass vacuously.
36
+ expect(files.every((f) => program.getSourceFile(f) !== undefined)).toBe(true);
37
+ return diagnostics.map((d) => {
38
+ const at = real(d.file!.fileName).slice(real(dir).length + 1);
39
+ return `${at}: ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
40
+ });
41
+ } finally {
42
+ rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ }
45
+
46
+ const generate = (yaml: string) => new OtelCollectorGenerator().generate(new OtelCollectorParser().parse(yaml));
47
+
48
+ /** Every vendored import fixture, by path under testdata/: the chant-built configs and the collector-contrib ones. */
49
+ function vendoredFixtures(): string[] {
50
+ const dir = join(import.meta.dirname, "testdata");
51
+ const yamlIn = (sub: string) =>
52
+ readdirSync(join(dir, sub))
53
+ .filter((f) => f.endsWith(".yaml"))
54
+ .sort()
55
+ .map((f) => (sub ? `${sub}/${f}` : f));
56
+ return [...yamlIn(""), ...yamlIn("upstream")];
57
+ }
58
+
59
+ describe("the generated source type-checks against the lexicon's config types", () => {
60
+ test("for every vendored fixture, every example's output, genAiPipeline() and every built-in's typed fields", async () => {
61
+ const fixtures = vendoredFixtures();
62
+ // The corpus the round-trip tests read; a missing directory would pass vacuously.
63
+ expect(fixtures).toEqual(expect.arrayContaining(["gateway.yaml", "upstream/couchbase.yaml", "upstream/servicegraph-nop.yaml"]));
64
+ expect(fixtures.length).toBeGreaterThanOrEqual(12);
65
+ const projects: Record<string, Array<{ path: string; content: string }>> = {
66
+ ...Object.fromEntries(fixtures.map((f) => [f, generate(read(...f.split("/")))])),
67
+ genai: generate(collectorYaml(genAiPipeline())),
68
+ everyBuiltin: generate(collectorYaml(everyBuiltin())),
69
+ ...Object.fromEntries((await exampleOutputs()).map(([n, y]) => [`example-${n}`, generate(y)])),
70
+ };
71
+ expect(typeErrors(projects)).toEqual([]);
72
+ }, 120_000);
73
+
74
+ test("a value outside a built-in's typed config is carried as found, and tsc points at it", () => {
75
+ // The type check is not vacuous: a batch timeout of `true` is neither a duration string nor
76
+ // nanoseconds, and a scrape job stays closed to keys Prometheus does not read.
77
+ const yaml = [
78
+ "receivers:",
79
+ " otlp:",
80
+ " protocols:",
81
+ " grpc: {}",
82
+ " prometheus:",
83
+ " config:",
84
+ " scrape_configs:",
85
+ " - job_name: self",
86
+ " static_configs: [{ targets: [\"localhost:8888\"] }]",
87
+ " basic_auth: { username: u, password_file: /etc/p }",
88
+ " scrape_everything: true",
89
+ "processors:",
90
+ " batch:",
91
+ " timeout: true",
92
+ "exporters:",
93
+ " debug: {}",
94
+ "service:",
95
+ " pipelines:",
96
+ " traces:",
97
+ " receivers: [otlp]",
98
+ " processors: [batch]",
99
+ " exporters: [debug]",
100
+ " metrics:",
101
+ " receivers: [prometheus]",
102
+ " exporters: [debug]",
103
+ "",
104
+ ].join("\n");
105
+ expect(typeErrors({ bad: generate(yaml) })).toEqual([
106
+ expect.stringMatching(/^bad\/processors\.ts: Type 'true' is not assignable to type 'Duration/),
107
+ expect.stringMatching(/^bad\/receivers\.ts: .*'scrape_everything' does not exist in type 'PrometheusScrapeConfig'/),
108
+ ]);
109
+ }, 120_000);
110
+ });