@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.
- package/README.md +5 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/common.d.ts +7 -2
- package/dist/components/common.d.ts.map +1 -1
- package/dist/components/filtering.d.ts +58 -1
- package/dist/components/filtering.d.ts.map +1 -1
- package/dist/components/processors.d.ts +10 -0
- package/dist/components/processors.d.ts.map +1 -1
- package/dist/components/receivers.d.ts +87 -7
- package/dist/components/receivers.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +12 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +39 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +61 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/integrity.json +4 -4
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +46 -17
- package/dist/skills/chant-otel.md +11 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +2 -1
- package/src/components/common.ts +7 -2
- package/src/components/filtering.test.ts +27 -0
- package/src/components/filtering.ts +65 -2
- package/src/components/processors.ts +4 -0
- package/src/components/receivers.ts +88 -4
- package/src/import/embedded.test.ts +56 -0
- package/src/import/embedded.ts +41 -0
- package/src/import/generated-types.e2e.test.ts +110 -0
- package/src/import/generator.test.ts +165 -0
- package/src/import/generator.ts +509 -0
- package/src/import/parser.test.ts +174 -0
- package/src/import/parser.ts +236 -0
- package/src/import/roundtrip.test.ts +365 -0
- package/src/import/testdata/agent-observability-agent.yaml +54 -0
- package/src/import/testdata/agent-observability-gateway.yaml +128 -0
- package/src/import/testdata/fixtures.ts +274 -0
- package/src/import/testdata/gateway.yaml +153 -0
- package/src/import/testdata/upstream/couchbase.yaml +150 -0
- package/src/import/testdata/upstream/fault-tolerant-logs.yaml +27 -0
- package/src/import/testdata/upstream/kubernetes-filelog.yaml +86 -0
- package/src/import/testdata/upstream/loadbalancing-agent.yaml +38 -0
- package/src/import/testdata/upstream/loadbalancing-backend.yaml +27 -0
- package/src/import/testdata/upstream/logline-filter-in.yaml +20 -0
- package/src/import/testdata/upstream/logline-filter-out.yaml +20 -0
- package/src/import/testdata/upstream/secure-tracing.yaml +26 -0
- package/src/import/testdata/upstream/servicegraph-nop.yaml +28 -0
- package/src/lint/rules/literal-credential.ts +46 -17
- package/src/lint/rules/rules.test.ts +16 -0
- package/src/plugin.ts +15 -0
- package/src/skills/chant-otel.md +11 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round trips through `chant import`.
|
|
3
|
+
*
|
|
4
|
+
* YAML -> TypeScript -> `chant build` -> YAML must give back the same config
|
|
5
|
+
* (key order and quoting aside) for every fixture: the otel examples' built
|
|
6
|
+
* output, a gateway with tail_sampling, loadbalancing and spanmetrics,
|
|
7
|
+
* `genAiPipeline()` output, the two collector configs of
|
|
8
|
+
* examples/agent-observability, and collector-contrib example configs at the
|
|
9
|
+
* pinned release. The generated source must lint clean.
|
|
10
|
+
*
|
|
11
|
+
* The other direction too: a config using every built-in's typed fields
|
|
12
|
+
* survives TypeScript -> YAML -> TypeScript -> YAML.
|
|
13
|
+
*
|
|
14
|
+
* Where `otelcol-contrib` is on PATH (or `OTELCOL_BIN` names a contrib
|
|
15
|
+
* build) the re-emitted configs are also checked by `otelcol validate`.
|
|
16
|
+
* generated-types.e2e.test.ts type-checks the generated source.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, expect, test } from "vitest";
|
|
20
|
+
import { spawnSync } from "child_process";
|
|
21
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
22
|
+
import { tmpdir } from "os";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
import { load } from "js-yaml";
|
|
25
|
+
import { build } from "@intentius/chant/build";
|
|
26
|
+
import { lintCommand } from "@intentius/chant/cli/commands/lint";
|
|
27
|
+
import { importCommand, importFromContent } from "@intentius/chant/cli/commands/import";
|
|
28
|
+
import { otelSerializer } from "../serializer";
|
|
29
|
+
import { collectorYaml } from "../collector";
|
|
30
|
+
import { registeredDefinitions } from "../define";
|
|
31
|
+
import { genAiPipeline } from "../genai";
|
|
32
|
+
import { SECTION_OF, type CollectorConfig, type ComponentKind } from "../model";
|
|
33
|
+
import * as c from "../components";
|
|
34
|
+
import { OtelCollectorParser } from "./parser";
|
|
35
|
+
import { OtelCollectorGenerator } from "./generator";
|
|
36
|
+
import { everyBuiltin, exampleOutputs, pkgDir, primary, read, UPSTREAM_BUILTIN_ONLY } from "./testdata/fixtures";
|
|
37
|
+
|
|
38
|
+
// ── helpers ─────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** A parsed config with the differences the collector does not see taken out. */
|
|
41
|
+
function normalize(yaml: string): CollectorConfig {
|
|
42
|
+
const doc = (load(yaml) ?? {}) as CollectorConfig;
|
|
43
|
+
for (const section of Object.values(SECTION_OF)) {
|
|
44
|
+
const s = doc[section];
|
|
45
|
+
if (!s) continue;
|
|
46
|
+
for (const id of Object.keys(s)) if (s[id] === null) s[id] = {};
|
|
47
|
+
if (Object.keys(s).length === 0) delete doc[section];
|
|
48
|
+
}
|
|
49
|
+
const service = doc.service;
|
|
50
|
+
if (service) {
|
|
51
|
+
if (service.extensions?.length === 0) delete service.extensions;
|
|
52
|
+
for (const p of Object.values(service.pipelines ?? {})) {
|
|
53
|
+
p.receivers ??= [];
|
|
54
|
+
p.exporters ??= [];
|
|
55
|
+
if (!p.processors || p.processors.length === 0) delete p.processors;
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(service).length === 0) delete doc.service;
|
|
58
|
+
}
|
|
59
|
+
return doc;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The `# chant:` header lines, with the component list of a semconv line
|
|
64
|
+
* sorted: the rebuilt config lists components in the order the build
|
|
65
|
+
* discovers them, which within a module is export-name order.
|
|
66
|
+
*/
|
|
67
|
+
function headerLines(yaml: string): string[] {
|
|
68
|
+
return yaml
|
|
69
|
+
.split("\n")
|
|
70
|
+
.filter((l) => /^#\s*chant:/.test(l))
|
|
71
|
+
.map((l) => l.replace(/\(([^)]*)\)$/, (_, list: string) => `(${list.split(", ").sort().join(", ")})`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface Imported {
|
|
75
|
+
source: string;
|
|
76
|
+
warnings: string[];
|
|
77
|
+
yaml: string;
|
|
78
|
+
buildErrors: unknown[];
|
|
79
|
+
lint: { errorCount: number; warningCount: number; output: string };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A chant project dir inside the package, so the lexicon resolves as it does for a user. */
|
|
83
|
+
function projectDir(): string {
|
|
84
|
+
const dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
|
|
85
|
+
mkdirSync(join(dir, "src"));
|
|
86
|
+
writeFileSync(join(dir, "chant.config.ts"), 'export default { lexicons: ["otel"] };\n');
|
|
87
|
+
writeFileSync(join(dir, "package.json"), '{ "name": "otel-import-roundtrip", "private": true, "type": "module" }\n');
|
|
88
|
+
return dir;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** YAML -> IR -> TypeScript -> `chant build` -> YAML, and `chant lint` over the source. */
|
|
92
|
+
async function importAndBuild(yaml: string): Promise<Imported> {
|
|
93
|
+
const ir = new OtelCollectorParser().parse(yaml);
|
|
94
|
+
const files = new OtelCollectorGenerator().generate(ir);
|
|
95
|
+
const dir = projectDir();
|
|
96
|
+
try {
|
|
97
|
+
const srcDir = join(dir, "src");
|
|
98
|
+
for (const file of files) writeFileSync(join(srcDir, file.path), file.content);
|
|
99
|
+
const result = await build(srcDir, [otelSerializer]);
|
|
100
|
+
const lint = await lintCommand({ path: srcDir, format: "stylish" });
|
|
101
|
+
return {
|
|
102
|
+
source: files.map((f) => `// ${f.path}\n${f.content}`).join("\n"),
|
|
103
|
+
warnings: ir.warnings ?? [],
|
|
104
|
+
yaml: primary(result.outputs.get("otel")),
|
|
105
|
+
buildErrors: result.errors,
|
|
106
|
+
lint: { errorCount: lint.errorCount, warningCount: lint.warningCount, output: lint.output },
|
|
107
|
+
};
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Every import is expected to rebuild equal and lint clean; a fixture may name lint rules it expects to fire. */
|
|
114
|
+
async function expectRoundTrip(yaml: string, opts: { lintRules?: string[] } = {}): Promise<Imported> {
|
|
115
|
+
const out = await importAndBuild(yaml);
|
|
116
|
+
expect(out.buildErrors).toEqual([]);
|
|
117
|
+
expect(normalize(out.yaml)).toEqual(normalize(yaml));
|
|
118
|
+
if (opts.lintRules) {
|
|
119
|
+
for (const rule of opts.lintRules) expect(out.lint.output).toContain(rule);
|
|
120
|
+
} else {
|
|
121
|
+
if (out.lint.errorCount + out.lint.warningCount > 0) console.log(out.lint.output);
|
|
122
|
+
expect(out.lint.errorCount).toBe(0);
|
|
123
|
+
expect(out.lint.warningCount).toBe(0);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findOtelcol(): string | undefined {
|
|
129
|
+
const candidates = [process.env.OTELCOL_BIN, "otelcol-contrib"].filter((b): b is string => !!b);
|
|
130
|
+
for (const bin of candidates) {
|
|
131
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf-8" });
|
|
132
|
+
if (r.status === 0) return bin;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const OTELCOL = findOtelcol();
|
|
138
|
+
|
|
139
|
+
function otelcolValidate(yaml: string): { ok: boolean; output: string } {
|
|
140
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-otelcol-import-"));
|
|
141
|
+
try {
|
|
142
|
+
const file = join(dir, "config.yaml");
|
|
143
|
+
writeFileSync(file, yaml);
|
|
144
|
+
const r = spawnSync(OTELCOL!, ["validate", `--config=${file}`], {
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
timeout: 60_000,
|
|
147
|
+
env: { ...process.env, TEMPO_TOKEN: "t", DEPLOY_ENV: "test", SPLUNK_HEC_TOKEN: "t", K8S_NODE_NAME: "node-1" },
|
|
148
|
+
});
|
|
149
|
+
return { ok: r.status === 0, output: `${r.stdout ?? ""}${r.stderr ?? ""}` };
|
|
150
|
+
} finally {
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe("YAML -> TypeScript -> YAML", () => {
|
|
156
|
+
test("the otel examples' built output", async () => {
|
|
157
|
+
const outputs = await exampleOutputs();
|
|
158
|
+
expect(outputs.map(([n]) => n)).toEqual(["custom-component", "genai-agent", "getting-started", "k8s-node-agent"]);
|
|
159
|
+
for (const [name, yaml] of outputs) {
|
|
160
|
+
const out = await expectRoundTrip(yaml);
|
|
161
|
+
// The `# chant:` header comes back too: the custom component's pin, the semconv line.
|
|
162
|
+
expect(headerLines(out.yaml), name).toEqual(headerLines(yaml));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a gateway with tail_sampling, loadbalancing and spanmetrics", async () => {
|
|
167
|
+
const yaml = read("gateway.yaml");
|
|
168
|
+
const out = await expectRoundTrip(yaml);
|
|
169
|
+
expect(out.source).toContain("new TailSamplingProcessor(");
|
|
170
|
+
expect(out.source).toContain("new LoadBalancingExporter(");
|
|
171
|
+
expect(out.source).toContain("const spanmetrics = new SpanMetricsConnector(");
|
|
172
|
+
// The connector is one entity, on both sides of the join.
|
|
173
|
+
expect(out.source).toContain("exporters: [otlpTempo, spanmetrics]");
|
|
174
|
+
expect(out.source).toContain("receivers: [spanmetrics]");
|
|
175
|
+
// ${env:VAR} stays a reference.
|
|
176
|
+
expect(out.source).toContain('authorization: "Bearer ${env:TEMPO_TOKEN}"');
|
|
177
|
+
expect(out.source).toContain('default: "${env:DEPLOY_ENV}"');
|
|
178
|
+
// pprof is declared but not enabled, so the Service lists what is.
|
|
179
|
+
expect(out.source).toContain("extensions: [zpages, healthCheck]");
|
|
180
|
+
expect(out.source).toContain("const telemetry: ServiceTelemetry = {");
|
|
181
|
+
// Nested config is lifted into named consts typed by the component's config type (COR001).
|
|
182
|
+
expect(out.source).toContain('const tailSamplingPolicies: TailSamplingProcessorConfig["policies"] = [');
|
|
183
|
+
expect(out.warnings).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("genAiPipeline() output", async () => {
|
|
187
|
+
const tempo = new c.OtlpExporter({ name: "tempo", endpoint: "tempo:4317", tls: { insecure: true } });
|
|
188
|
+
const sampler = new c.ProbabilisticSamplerProcessor({ sampling_percentage: 25 });
|
|
189
|
+
for (const entities of [
|
|
190
|
+
genAiPipeline(),
|
|
191
|
+
genAiPipeline({ traceExporters: [tempo], sampling: [sampler], keepContent: true, logs: false }),
|
|
192
|
+
genAiPipeline({ maskValues: ["\\b[0-9]{13,16}\\b"], hashFunction: "sha3", healthCheck: false }),
|
|
193
|
+
]) {
|
|
194
|
+
const yaml = collectorYaml(entities);
|
|
195
|
+
const out = await expectRoundTrip(yaml);
|
|
196
|
+
expect(headerLines(out.yaml)).toEqual(headerLines(yaml));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("the collector configs of examples/agent-observability", async () => {
|
|
201
|
+
for (const file of ["agent-observability-agent.yaml", "agent-observability-gateway.yaml"]) {
|
|
202
|
+
await expectRoundTrip(read(file));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
for (const file of UPSTREAM_BUILTIN_ONLY) {
|
|
207
|
+
test(`collector-contrib v0.130.0: ${file}`, async () => {
|
|
208
|
+
const out = await expectRoundTrip(read("upstream", file));
|
|
209
|
+
expect(out.source).not.toContain("defineComponent");
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
test.skipIf(!OTELCOL)("otelcol validate accepts every re-emitted config", async () => {
|
|
214
|
+
// Not the agent-observability agent: its k8s resolver needs a cluster to build (that example's own tests swap it for dns).
|
|
215
|
+
const yamls: Array<[string, string]> = [
|
|
216
|
+
["gateway.yaml", read("gateway.yaml")],
|
|
217
|
+
["agent-observability-gateway.yaml", read("agent-observability-gateway.yaml")],
|
|
218
|
+
["genAiPipeline()", collectorYaml(genAiPipeline())],
|
|
219
|
+
...UPSTREAM_BUILTIN_ONLY.map((f): [string, string] => [f, read("upstream", f)]),
|
|
220
|
+
// hostmetrics' root_path is accepted on linux only, which is where the node agent runs.
|
|
221
|
+
...(await exampleOutputs()).filter(([n]) => n !== "k8s-node-agent" || process.platform === "linux"),
|
|
222
|
+
];
|
|
223
|
+
for (const [name, yaml] of yamls) {
|
|
224
|
+
const { yaml: rebuilt } = await importAndBuild(yaml);
|
|
225
|
+
const { ok, output } = otelcolValidate(rebuilt);
|
|
226
|
+
expect(ok, `${name}: ${output}`).toBe(true);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("components chant does not ship", () => {
|
|
233
|
+
test("go through defineComponent with their config as data, and survive the round trip", async () => {
|
|
234
|
+
const out = await expectRoundTrip(read("upstream", "fault-tolerant-logs.yaml"));
|
|
235
|
+
expect(out.source).toContain("const FileStorageExtension = defineComponent<Record<string, unknown>>()({");
|
|
236
|
+
expect(out.source).toContain("pin: COLLECTOR_PIN,");
|
|
237
|
+
expect(out.source).toMatch(/\/\/ extension "file_storage" is not a component chant ships/);
|
|
238
|
+
expect(out.source).toContain('const fileStorageFilelogreceiver = new FileStorageExtension({');
|
|
239
|
+
expect(out.source).toContain('import { FileStorageExtension } from "./custom-components";');
|
|
240
|
+
// The typed built-ins keep referring to the storage extension by id, as data.
|
|
241
|
+
expect(out.source).toContain('storage: "file_storage/filelogreceiver"');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("an unknown receiver and exporter in the loadbalancing agent example", async () => {
|
|
245
|
+
const out = await expectRoundTrip(read("upstream", "loadbalancing-agent.yaml"));
|
|
246
|
+
expect(out.source).toContain('type: "fluentforward"');
|
|
247
|
+
expect(out.source).toContain("new LoadBalancingExporter(");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("nop receivers and exporters around a servicegraph connector", async () => {
|
|
251
|
+
const out = await expectRoundTrip(read("upstream", "servicegraph-nop.yaml"));
|
|
252
|
+
expect(out.source).toContain("const NopReceiver = defineComponent");
|
|
253
|
+
expect(out.source).toContain("const NopExporter = defineComponent");
|
|
254
|
+
expect(out.source).toContain("const servicegraph = new ServiceGraphConnector(");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a literal credential is imported as found and reported by OTEL002", async () => {
|
|
258
|
+
const out = await expectRoundTrip(read("upstream", "couchbase.yaml"), { lintRules: ["OTEL002"] });
|
|
259
|
+
expect(out.source).toContain('password: "otelpassword"');
|
|
260
|
+
expect(out.source).toContain("const MetricstransformProcessor = defineComponent");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("a pin from the `# chant:` header is carried back into defineComponent", async () => {
|
|
264
|
+
const yaml = [
|
|
265
|
+
"# chant: exporter datadog/eu schema github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter@v0.129.0 sha256:abc",
|
|
266
|
+
"receivers:",
|
|
267
|
+
" otlp:",
|
|
268
|
+
" protocols:",
|
|
269
|
+
" grpc: {}",
|
|
270
|
+
"exporters:",
|
|
271
|
+
" datadog/eu:",
|
|
272
|
+
" api:",
|
|
273
|
+
" key: ${env:DD_API_KEY}",
|
|
274
|
+
" site: datadoghq.eu",
|
|
275
|
+
"service:",
|
|
276
|
+
" pipelines:",
|
|
277
|
+
" traces:",
|
|
278
|
+
" receivers: [otlp]",
|
|
279
|
+
" exporters: [datadog/eu]",
|
|
280
|
+
"",
|
|
281
|
+
].join("\n");
|
|
282
|
+
const out = await expectRoundTrip(yaml);
|
|
283
|
+
expect(out.source).toContain('version: "v0.129.0"');
|
|
284
|
+
expect(out.source).toContain('digest: "sha256:abc"');
|
|
285
|
+
expect(out.source).not.toContain("COLLECTOR_PIN,");
|
|
286
|
+
expect(headerLines(out.yaml)).toEqual(headerLines(yaml));
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("TypeScript -> YAML -> TypeScript -> YAML", () => {
|
|
291
|
+
test("every built-in, each using its typed fields", async () => {
|
|
292
|
+
const entities = everyBuiltin();
|
|
293
|
+
const first = collectorYaml(entities);
|
|
294
|
+
|
|
295
|
+
// The fixture covers every built-in this package registers.
|
|
296
|
+
const config = load(first) as CollectorConfig;
|
|
297
|
+
const used = new Set<string>();
|
|
298
|
+
for (const [kind, section] of Object.entries(SECTION_OF) as Array<[ComponentKind, keyof CollectorConfig]>) {
|
|
299
|
+
for (const id of Object.keys((config[section] as Record<string, unknown>) ?? {})) used.add(`${kind}:${id.split("/")[0]}`);
|
|
300
|
+
}
|
|
301
|
+
const builtins = registeredDefinitions().filter((d) => d.builtin).map((d) => `${d.kind}:${d.type}`);
|
|
302
|
+
expect([...used].sort()).toEqual([...new Set(builtins)].sort());
|
|
303
|
+
|
|
304
|
+
const out = await expectRoundTrip(first);
|
|
305
|
+
expect(out.source).not.toContain("defineComponent");
|
|
306
|
+
// Importing is a fixed point from there: the rebuilt YAML (components now in
|
|
307
|
+
// export-name order within each section) gives the same TypeScript and YAML, text for text.
|
|
308
|
+
const again = await importAndBuild(out.yaml);
|
|
309
|
+
const third = await importAndBuild(again.yaml);
|
|
310
|
+
expect(third.source).toBe(again.source);
|
|
311
|
+
expect(third.yaml).toBe(again.yaml);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ── through core's import command ───────────────────────────────────
|
|
316
|
+
|
|
317
|
+
test("importFromContent writes one module per section through the otel plugin", async () => {
|
|
318
|
+
const dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
|
|
319
|
+
try {
|
|
320
|
+
const output = join(dir, "src");
|
|
321
|
+
const result = await importFromContent({ content: read("gateway.yaml"), lexicon: "otel", output });
|
|
322
|
+
expect(result.error).toBeUndefined();
|
|
323
|
+
expect(result.success).toBe(true);
|
|
324
|
+
expect(result.lexicon).toBe("otel");
|
|
325
|
+
expect(result.generatedFiles).toEqual([
|
|
326
|
+
"receivers.ts",
|
|
327
|
+
"processors.ts",
|
|
328
|
+
"exporters.ts",
|
|
329
|
+
"connectors.ts",
|
|
330
|
+
"extensions.ts",
|
|
331
|
+
"pipelines.ts",
|
|
332
|
+
"service.ts",
|
|
333
|
+
]);
|
|
334
|
+
const built = await build(output, [otelSerializer]);
|
|
335
|
+
expect(built.errors).toEqual([]);
|
|
336
|
+
expect(normalize(primary(built.outputs.get("otel")))).toEqual(normalize(read("gateway.yaml")));
|
|
337
|
+
} finally {
|
|
338
|
+
rmSync(dir, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("chant import collector.yaml", () => {
|
|
343
|
+
for (const lexicon of [undefined, "otel"]) {
|
|
344
|
+
test(lexicon ? "with --lexicon otel" : "detected as a collector config", async () => {
|
|
345
|
+
const dir = projectDir();
|
|
346
|
+
try {
|
|
347
|
+
const templatePath = join(dir, "collector.yaml");
|
|
348
|
+
writeFileSync(templatePath, read("gateway.yaml"));
|
|
349
|
+
const output = join(dir, "src");
|
|
350
|
+
const result = await importCommand({ templatePath, output, force: true, lexicon });
|
|
351
|
+
expect(result.error).toBeUndefined();
|
|
352
|
+
expect(result.success).toBe(true);
|
|
353
|
+
expect(result.lexicon).toBe("otel");
|
|
354
|
+
expect(result.generatedFiles).toContain("pipelines.ts");
|
|
355
|
+
const built = await build(output, [otelSerializer]);
|
|
356
|
+
expect(built.errors).toEqual([]);
|
|
357
|
+
expect(normalize(primary(built.outputs.get("otel")))).toEqual(normalize(read("gateway.yaml")));
|
|
358
|
+
const lint = await lintCommand({ path: output, format: "stylish" });
|
|
359
|
+
expect(lint.errorCount + lint.warningCount, lint.output).toBe(0);
|
|
360
|
+
} finally {
|
|
361
|
+
rmSync(dir, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# The otel-agent-config ConfigMap's config.yaml from examples/agent-observability,
|
|
2
|
+
# as that example builds it. Regenerate from examples/agent-observability/test/built.ts (agentConfigYaml).
|
|
3
|
+
|
|
4
|
+
receivers:
|
|
5
|
+
otlp:
|
|
6
|
+
protocols:
|
|
7
|
+
grpc:
|
|
8
|
+
endpoint: 0.0.0.0:4317
|
|
9
|
+
http:
|
|
10
|
+
endpoint: 0.0.0.0:4318
|
|
11
|
+
|
|
12
|
+
processors:
|
|
13
|
+
memory_limiter:
|
|
14
|
+
check_interval: 1s
|
|
15
|
+
limit_percentage: 80
|
|
16
|
+
spike_limit_percentage: 20
|
|
17
|
+
batch:
|
|
18
|
+
timeout: 1s
|
|
19
|
+
|
|
20
|
+
exporters:
|
|
21
|
+
loadbalancing/gateway:
|
|
22
|
+
routing_key: traceID
|
|
23
|
+
protocol:
|
|
24
|
+
otlp:
|
|
25
|
+
tls:
|
|
26
|
+
insecure: true
|
|
27
|
+
resolver:
|
|
28
|
+
k8s:
|
|
29
|
+
service: otel-gateway-headless.observability
|
|
30
|
+
ports: [4317]
|
|
31
|
+
otlp/gateway:
|
|
32
|
+
endpoint: otel-gateway.observability.svc:4317
|
|
33
|
+
tls:
|
|
34
|
+
insecure: true
|
|
35
|
+
|
|
36
|
+
extensions:
|
|
37
|
+
health_check:
|
|
38
|
+
endpoint: 0.0.0.0:13133
|
|
39
|
+
|
|
40
|
+
service:
|
|
41
|
+
extensions: [health_check]
|
|
42
|
+
pipelines:
|
|
43
|
+
traces:
|
|
44
|
+
receivers: [otlp]
|
|
45
|
+
processors: [memory_limiter, batch]
|
|
46
|
+
exporters: [loadbalancing/gateway]
|
|
47
|
+
metrics:
|
|
48
|
+
receivers: [otlp]
|
|
49
|
+
processors: [memory_limiter, batch]
|
|
50
|
+
exporters: [otlp/gateway]
|
|
51
|
+
logs:
|
|
52
|
+
receivers: [otlp]
|
|
53
|
+
processors: [memory_limiter, batch]
|
|
54
|
+
exporters: [otlp/gateway]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# The otel-gateway-config ConfigMap's config.yaml from examples/agent-observability,
|
|
2
|
+
# as that example builds it. Regenerate from examples/agent-observability/test/built.ts (gatewayConfigYaml).
|
|
3
|
+
|
|
4
|
+
# chant: semconv gen_ai github.com/open-telemetry/semantic-conventions@v1.41.1 (transform/genai_content, redaction/genai_content, filter/genai_spans, spanmetrics/genai, sum/genai_tokens)
|
|
5
|
+
|
|
6
|
+
receivers:
|
|
7
|
+
otlp:
|
|
8
|
+
protocols:
|
|
9
|
+
grpc:
|
|
10
|
+
endpoint: 0.0.0.0:4317
|
|
11
|
+
|
|
12
|
+
processors:
|
|
13
|
+
memory_limiter:
|
|
14
|
+
check_interval: 1s
|
|
15
|
+
limit_percentage: 80
|
|
16
|
+
spike_limit_percentage: 20
|
|
17
|
+
transform/genai_content:
|
|
18
|
+
error_mode: ignore
|
|
19
|
+
trace_statements:
|
|
20
|
+
- context: span
|
|
21
|
+
statements: ["delete_key(span.attributes, \"gen_ai.system_instructions\")", "delete_key(span.attributes, \"gen_ai.input.messages\")", "delete_key(span.attributes, \"gen_ai.output.messages\")", "delete_key(span.attributes, \"gen_ai.tool.call.arguments\")", "delete_key(span.attributes, \"gen_ai.tool.call.result\")", "delete_key(span.attributes, \"gen_ai.retrieval.query.text\")", "delete_key(span.attributes, \"gen_ai.retrieval.documents\")", "delete_key(span.attributes, \"gen_ai.prompt\")", "delete_key(span.attributes, \"gen_ai.completion\")", "delete_matching_keys(span.attributes, \"^gen_ai\\\\.(prompt|completion)\\\\.[0-9]+\\\\..+$\")"]
|
|
22
|
+
- context: spanevent
|
|
23
|
+
statements: ["delete_key(spanevent.attributes, \"gen_ai.system_instructions\")", "delete_key(spanevent.attributes, \"gen_ai.input.messages\")", "delete_key(spanevent.attributes, \"gen_ai.output.messages\")", "delete_key(spanevent.attributes, \"gen_ai.tool.call.arguments\")", "delete_key(spanevent.attributes, \"gen_ai.tool.call.result\")", "delete_key(spanevent.attributes, \"gen_ai.retrieval.query.text\")", "delete_key(spanevent.attributes, \"gen_ai.retrieval.documents\")", "delete_key(spanevent.attributes, \"gen_ai.prompt\")", "delete_key(spanevent.attributes, \"gen_ai.completion\")", "delete_matching_keys(spanevent.attributes, \"^gen_ai\\\\.(prompt|completion)\\\\.[0-9]+\\\\..+$\")"]
|
|
24
|
+
log_statements:
|
|
25
|
+
- context: log
|
|
26
|
+
statements: ["delete_key(log.attributes, \"gen_ai.system_instructions\")", "delete_key(log.attributes, \"gen_ai.input.messages\")", "delete_key(log.attributes, \"gen_ai.output.messages\")", "delete_key(log.attributes, \"gen_ai.tool.call.arguments\")", "delete_key(log.attributes, \"gen_ai.tool.call.result\")", "delete_key(log.attributes, \"gen_ai.retrieval.query.text\")", "delete_key(log.attributes, \"gen_ai.retrieval.documents\")", "delete_key(log.attributes, \"gen_ai.prompt\")", "delete_key(log.attributes, \"gen_ai.completion\")", "delete_matching_keys(log.attributes, \"^gen_ai\\\\.(prompt|completion)\\\\.[0-9]+\\\\..+$\")", "delete_matching_keys(log.body, \"^(content|message|tool_calls)$\") where IsMap(log.body) and (IsMatch(log.event_name, \"^(gen_ai\\\\.system\\\\.message|gen_ai\\\\.user\\\\.message|gen_ai\\\\.assistant\\\\.message|gen_ai\\\\.tool\\\\.message|gen_ai\\\\.choice)$\") or IsMatch(log.attributes[\"event.name\"], \"^(gen_ai\\\\.system\\\\.message|gen_ai\\\\.user\\\\.message|gen_ai\\\\.assistant\\\\.message|gen_ai\\\\.tool\\\\.message|gen_ai\\\\.choice)$\"))", "set(log.body, \"\") where IsString(log.body) and (IsMatch(log.event_name, \"^(gen_ai\\\\.system\\\\.message|gen_ai\\\\.user\\\\.message|gen_ai\\\\.assistant\\\\.message|gen_ai\\\\.tool\\\\.message|gen_ai\\\\.choice)$\") or IsMatch(log.attributes[\"event.name\"], \"^(gen_ai\\\\.system\\\\.message|gen_ai\\\\.user\\\\.message|gen_ai\\\\.assistant\\\\.message|gen_ai\\\\.tool\\\\.message|gen_ai\\\\.choice)$\"))"]
|
|
27
|
+
redaction/genai_content:
|
|
28
|
+
allow_all_keys: true
|
|
29
|
+
blocked_key_patterns: [^(gen_ai\.system_instructions|gen_ai\.input\.messages|gen_ai\.output\.messages|gen_ai\.tool\.call\.arguments|gen_ai\.tool\.call\.result|gen_ai\.retrieval\.query\.text|gen_ai\.retrieval\.documents|gen_ai\.prompt|gen_ai\.completion)$, "^gen_ai\\.(prompt|completion)\\.[0-9]+\\..+$"]
|
|
30
|
+
tail_sampling:
|
|
31
|
+
decision_wait: 5s
|
|
32
|
+
num_traces: 50000
|
|
33
|
+
policies:
|
|
34
|
+
- name: errors
|
|
35
|
+
type: status_code
|
|
36
|
+
status_code:
|
|
37
|
+
status_codes: [ERROR]
|
|
38
|
+
- name: slow
|
|
39
|
+
type: latency
|
|
40
|
+
latency:
|
|
41
|
+
threshold_ms: 2000
|
|
42
|
+
- name: baseline
|
|
43
|
+
type: probabilistic
|
|
44
|
+
probabilistic:
|
|
45
|
+
sampling_percentage: 10
|
|
46
|
+
batch:
|
|
47
|
+
timeout: 5s
|
|
48
|
+
filter/genai_spans:
|
|
49
|
+
error_mode: ignore
|
|
50
|
+
traces:
|
|
51
|
+
span: ["attributes[\"gen_ai.operation.name\"] == nil"]
|
|
52
|
+
|
|
53
|
+
exporters:
|
|
54
|
+
otlp/tempo:
|
|
55
|
+
endpoint: tempo:4317
|
|
56
|
+
tls:
|
|
57
|
+
insecure: true
|
|
58
|
+
prometheus:
|
|
59
|
+
endpoint: 0.0.0.0:8889
|
|
60
|
+
metric_expiration: 10m
|
|
61
|
+
otlphttp/loki:
|
|
62
|
+
endpoint: http://loki:3100/otlp
|
|
63
|
+
|
|
64
|
+
connectors:
|
|
65
|
+
spanmetrics:
|
|
66
|
+
histogram:
|
|
67
|
+
unit: ms
|
|
68
|
+
explicit:
|
|
69
|
+
buckets: [50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s]
|
|
70
|
+
metrics_flush_interval: 15s
|
|
71
|
+
forward/genai: {}
|
|
72
|
+
forward/sampled: {}
|
|
73
|
+
spanmetrics/genai:
|
|
74
|
+
namespace: genai
|
|
75
|
+
dimensions:
|
|
76
|
+
- name: gen_ai.operation.name
|
|
77
|
+
- name: gen_ai.request.model
|
|
78
|
+
- name: gen_ai.tool.name
|
|
79
|
+
- name: error.type
|
|
80
|
+
histogram:
|
|
81
|
+
unit: s
|
|
82
|
+
explicit:
|
|
83
|
+
buckets: [100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 20s, 40s, 80s]
|
|
84
|
+
metrics_flush_interval: 15s
|
|
85
|
+
sum/genai_tokens:
|
|
86
|
+
spans:
|
|
87
|
+
genai.tokens.input:
|
|
88
|
+
source_attribute: gen_ai.usage.input_tokens
|
|
89
|
+
description: Input tokens used by GenAI operations
|
|
90
|
+
conditions: ["attributes[\"gen_ai.usage.input_tokens\"] != nil and not (kind == SPAN_KIND_INTERNAL and (attributes[\"gen_ai.operation.name\"] == \"invoke_agent\" or attributes[\"gen_ai.operation.name\"] == \"invoke_workflow\"))"]
|
|
91
|
+
attributes:
|
|
92
|
+
- key: gen_ai.request.model
|
|
93
|
+
default_value: unknown
|
|
94
|
+
genai.tokens.output:
|
|
95
|
+
source_attribute: gen_ai.usage.output_tokens
|
|
96
|
+
description: Output tokens used by GenAI operations
|
|
97
|
+
conditions: ["attributes[\"gen_ai.usage.output_tokens\"] != nil and not (kind == SPAN_KIND_INTERNAL and (attributes[\"gen_ai.operation.name\"] == \"invoke_agent\" or attributes[\"gen_ai.operation.name\"] == \"invoke_workflow\"))"]
|
|
98
|
+
attributes:
|
|
99
|
+
- key: gen_ai.request.model
|
|
100
|
+
default_value: unknown
|
|
101
|
+
|
|
102
|
+
extensions:
|
|
103
|
+
health_check:
|
|
104
|
+
endpoint: 0.0.0.0:13133
|
|
105
|
+
|
|
106
|
+
service:
|
|
107
|
+
extensions: [health_check]
|
|
108
|
+
pipelines:
|
|
109
|
+
traces:
|
|
110
|
+
receivers: [otlp]
|
|
111
|
+
processors: [memory_limiter, transform/genai_content, redaction/genai_content]
|
|
112
|
+
exporters: [spanmetrics, forward/genai, forward/sampled]
|
|
113
|
+
traces/sampled:
|
|
114
|
+
receivers: [forward/sampled]
|
|
115
|
+
processors: [tail_sampling, batch]
|
|
116
|
+
exporters: [otlp/tempo]
|
|
117
|
+
traces/genai:
|
|
118
|
+
receivers: [forward/genai]
|
|
119
|
+
processors: [filter/genai_spans]
|
|
120
|
+
exporters: [spanmetrics/genai, sum/genai_tokens]
|
|
121
|
+
metrics:
|
|
122
|
+
receivers: [otlp, spanmetrics, spanmetrics/genai, sum/genai_tokens]
|
|
123
|
+
processors: [memory_limiter, batch]
|
|
124
|
+
exporters: [prometheus]
|
|
125
|
+
logs:
|
|
126
|
+
receivers: [otlp]
|
|
127
|
+
processors: [memory_limiter, transform/genai_content, redaction/genai_content, batch]
|
|
128
|
+
exporters: [otlphttp/loki]
|