@intentius/chant-lexicon-otel 0.97.0 → 0.98.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/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/import/generated-types.e2e.test.ts +75 -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 +10 -0
- package/src/skills/chant-otel.md +11 -0
package/README.md
CHANGED
|
@@ -52,6 +52,10 @@ Each connector supports fixed signal pairs (`spanmetrics`: traces to metrics). O
|
|
|
52
52
|
|
|
53
53
|
`defineComponent<Config>()({ kind, type, pin, validate?, endpoints?, connects? })` returns a class that serializes and lints like a built-in. `pin` records the schema source and version the config type follows; it is written as a `# chant:` comment above the emitted config and returned by `collectorTopology()`.
|
|
54
54
|
|
|
55
|
+
## Importing an existing config
|
|
56
|
+
|
|
57
|
+
`chant import otel-collector-config.yaml --output src` turns a collector config into this lexicon's TypeScript: one module per section plus `pipelines.ts`, each component a constant of its built-in class (`otlp/tempo` becomes `const otlpTempo = new OtlpExporter({ name: "tempo", ... })`), each connector one constant listed on both sides of its join. `${env:VAR}` references stay references. A component type chant doesn't ship is declared with `defineComponent` in `custom-components.ts`, config carried as data, pinned to the `# chant:` header's pin when the file has one and to `COLLECTOR_PIN` otherwise. `chant build` on the result gives back the same config; the round-trip tests in `src/import/roundtrip.test.ts` hold every example, a sampling gateway, `genAiPipeline()` output and collector-contrib's example configs to that.
|
|
58
|
+
|
|
55
59
|
## Checks
|
|
56
60
|
|
|
57
61
|
OTEL001 and OTEL002 run on source (id syntax, literal credentials). OTEL101 to OTEL109 and OTEL112 run after a build: undeclared or unused components (a connector must be on both sides), empty pipelines, extension wiring, `memory_limiter` placement, id syntax, each component's own config rules, duplicate ids, missing schema pins, and connector signal pairs.
|
|
@@ -71,4 +75,5 @@ OTEL001 and OTEL002 run on source (id syntax, literal credentials). OTEL101 to O
|
|
|
71
75
|
- `src/validate-config.ts`, `src/lint/`: checks
|
|
72
76
|
- `src/topology.ts`, `src/semconv.ts`: the read surface
|
|
73
77
|
- `src/genai.ts`: the GenAI preset
|
|
78
|
+
- `src/import/`: `chant import` for collector YAML (parser, generator, round-trip tests and fixtures)
|
|
74
79
|
- `examples/`: getting-started, k8s-node-agent, genai-agent, custom-component
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsEH,wBAAsB,YAAY,CAAC,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB9E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TemplateIR` -> TypeScript, for `chant import`.
|
|
3
|
+
*
|
|
4
|
+
* Writes the collector the way the lexicon's examples are written: a module
|
|
5
|
+
* per section with one constant per component, typed by its built-in class
|
|
6
|
+
* (`const otlpTempo = new OtlpExporter({ name: "tempo", ... })`), nested
|
|
7
|
+
* settings lifted into consts typed by the component's config type (COR001),
|
|
8
|
+
* at most eight declarables per module (COR009), and an `export { ... }` list
|
|
9
|
+
* at the end of each. `pipelines.ts` declares a `Pipeline` per pipeline that
|
|
10
|
+
* references those constants, and `service.ts` a `Service` when
|
|
11
|
+
* `service.extensions` or `service.telemetry` needs one. A connector is one
|
|
12
|
+
* constant, listed in the exporters of the pipeline that feeds it and the
|
|
13
|
+
* receivers of the pipeline it feeds.
|
|
14
|
+
*
|
|
15
|
+
* A component type chant does not ship gets a `defineComponent` in
|
|
16
|
+
* `custom-components.ts`, with its config carried as data and a comment
|
|
17
|
+
* saying so. Its pin is the one the config's `# chant:` header named, or
|
|
18
|
+
* `COLLECTOR_PIN`.
|
|
19
|
+
*/
|
|
20
|
+
import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import/generator";
|
|
21
|
+
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
22
|
+
import { type CollectorConfig, type ComponentKind } from "../model.js";
|
|
23
|
+
import { type CollectorResourceMetadata } from "./parser.js";
|
|
24
|
+
/** The class name of the built-in for `kind` + `type`, if chant ships one. */
|
|
25
|
+
export declare function builtinClassName(kind: ComponentKind, type: string): string | undefined;
|
|
26
|
+
/** A value as a TypeScript expression, starting at column `col`, indented by `indent` spaces. */
|
|
27
|
+
export declare function tsLiteral(v: unknown, indent: number, col?: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Generate the TypeScript declaring one collector config: one module per
|
|
30
|
+
* section (split at eight declarables, as COR009 asks), `pipelines.ts`,
|
|
31
|
+
* `service.ts` when the config needs a Service, and `custom-components.ts`
|
|
32
|
+
* for component types chant does not ship.
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateCollectorFiles(config: CollectorConfig, meta?: Partial<CollectorResourceMetadata>): GeneratedFile[];
|
|
35
|
+
/** The OpenTelemetry Collector TypeScript generator `chant import` runs. */
|
|
36
|
+
export declare class OtelCollectorGenerator implements TypeScriptGenerator {
|
|
37
|
+
generate(ir: TemplateIR): GeneratedFile[];
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/import/generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC5F,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAG7E,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,UAAU,CAAC;AAsBlB,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEtF;AAsED,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,CAmB1E;AAoJD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,GAAE,OAAO,CAAC,yBAAyB,CAAM,GAAG,aAAa,EAAE,CA6L9H;AAED,4EAA4E;AAC5E,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,EAAE;CAQ1C"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector YAML -> `TemplateIR`, for `chant import`.
|
|
3
|
+
*
|
|
4
|
+
* A collector config is one document whose parts name each other: pipelines
|
|
5
|
+
* list component ids, a connector sits in two pipelines, `service.extensions`
|
|
6
|
+
* lists extension ids. So the IR carries the whole config as one resource of
|
|
7
|
+
* type `OTel::Collector`, and the generator lays out the modules itself, with
|
|
8
|
+
* those names turned into imports of declared entities. (Core splits an IR
|
|
9
|
+
* with more than three resources into one generate() call per category and
|
|
10
|
+
* keeps only the first file of each, which would lose the imports between a
|
|
11
|
+
* pipeline and the components it references.)
|
|
12
|
+
*
|
|
13
|
+
* The config is carried as parsed, with nothing dropped from a component's
|
|
14
|
+
* config. What the lexicon cannot express is named in `warnings`.
|
|
15
|
+
*
|
|
16
|
+
* The `# chant:` lines the serializer writes above a config are read back:
|
|
17
|
+
* a custom component's schema pin becomes the pin of the `defineComponent`
|
|
18
|
+
* the generator writes for it. A config without them imports against
|
|
19
|
+
* `COLLECTOR_PIN`.
|
|
20
|
+
*/
|
|
21
|
+
import type { TemplateIR, TemplateParser } from "@intentius/chant/import/parser";
|
|
22
|
+
import { type SchemaPin } from "../define.js";
|
|
23
|
+
import { type CollectorConfig, type ComponentKind } from "../model.js";
|
|
24
|
+
/** The IR resource type for a whole collector config. */
|
|
25
|
+
export declare const COLLECTOR_RESOURCE_TYPE = "OTel::Collector";
|
|
26
|
+
/** One `# chant: <kind> <id> schema <source>@<version>` header line, read back. */
|
|
27
|
+
export interface HeaderPin {
|
|
28
|
+
kind: ComponentKind;
|
|
29
|
+
id: string;
|
|
30
|
+
pin: SchemaPin;
|
|
31
|
+
}
|
|
32
|
+
/** One `# chant: semconv <namespace> <source>@<version>` header line, read back. */
|
|
33
|
+
export interface HeaderSemconv {
|
|
34
|
+
namespace: string;
|
|
35
|
+
pin: SchemaPin;
|
|
36
|
+
}
|
|
37
|
+
/** What the parser read from a collector YAML file. */
|
|
38
|
+
export interface ParsedCollector {
|
|
39
|
+
config: CollectorConfig;
|
|
40
|
+
pins: HeaderPin[];
|
|
41
|
+
semconv: HeaderSemconv[];
|
|
42
|
+
warnings: string[];
|
|
43
|
+
}
|
|
44
|
+
/** `properties` of the `OTel::Collector` resource. */
|
|
45
|
+
export interface CollectorResourceProperties {
|
|
46
|
+
config: CollectorConfig;
|
|
47
|
+
}
|
|
48
|
+
/** `metadata` of the `OTel::Collector` resource. */
|
|
49
|
+
export interface CollectorResourceMetadata {
|
|
50
|
+
pins: HeaderPin[];
|
|
51
|
+
semconv: HeaderSemconv[];
|
|
52
|
+
}
|
|
53
|
+
/** Parse collector YAML into the plain config model, the header pins and what cannot be carried. */
|
|
54
|
+
export declare function parseCollectorYaml(content: string): ParsedCollector;
|
|
55
|
+
/** Which kind a section's components are. */
|
|
56
|
+
export declare const KIND_OF_SECTION: Record<string, ComponentKind>;
|
|
57
|
+
/** The OpenTelemetry Collector config parser `chant import` runs. */
|
|
58
|
+
export declare class OtelCollectorParser implements TemplateParser {
|
|
59
|
+
parse(content: string): TemplateIR;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACjF,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,UAAU,CAAC;AAElB,yDAAyD;AACzD,eAAO,MAAM,uBAAuB,oBAAoB,CAAC;AAEzD,mFAAmF;AACnF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,SAAS,CAAC;CAChB;AAED,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,SAAS,CAAC;CAChB;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,sDAAsD;AACtD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,oDAAoD;AACpD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAoDD,oGAAoG;AACpG,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAyFnE;AAED,6CAA6C;AAC7C,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAEzD,CAAC;AAEF,qEAAqE;AACrE,qBAAa,mBAAoB,YAAW,cAAc;IACxD,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU;CAiBnC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "cb39957cd5d360c633841a23f79c656f248a58e5b696b23df6560b92b993ecf3",
|
|
5
5
|
"meta.json": "664490c2bee6c839f22994b789adc4708b01a37195f9c193dfb3c974959c5d74",
|
|
6
6
|
"types/index.d.ts": "a1070fd1cb3e8d8a9002fc5a75a80b3bdda02965be351e8aed2dbc2f63d36e3c",
|
|
7
7
|
"rules/component-id-syntax.ts": "95f507614d2626159140d906bce9d086e8ecba18ede22f5ddfbda13ac1c08169",
|
|
8
|
-
"rules/literal-credential.ts": "
|
|
8
|
+
"rules/literal-credential.ts": "48a235a772476bd56cdaaae3f6e90f8a9b2675317be6dcb48c89e54d2f7a5708",
|
|
9
9
|
"rules/otel-ast.ts": "cf286102f778bd010d130ad8de6adc279de517fffffcd49d7cfb3c419b4bec66",
|
|
10
10
|
"rules/otel-helpers.ts": "22f5ed00f5cf33638558eb4363c7deeeefa2cdc3e666404f5235caf471fb1466",
|
|
11
11
|
"rules/otel101.ts": "9a9d5f79403fb07e78752f904741ab4cec8c0f00975b44fe0b6eace63fda4c13",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"rules/otel108.ts": "5eddca667b3ec563a1a7bc745139738d438c4307b4861b999a4d8cff975856b6",
|
|
19
19
|
"rules/otel109.ts": "db025b5f09cc8c5199b420fdfd6e4bae0dd5abd1860903d8c9b4d625162a32b9",
|
|
20
20
|
"rules/otel112.ts": "0a6d9a7d6ba281f3b011dde18646f7c15280dcb8cfeddaa6d20c6080a2c92cb0",
|
|
21
|
-
"skills/chant-otel.md": "
|
|
21
|
+
"skills/chant-otel.md": "f77c11e59c9ee782525fd28b1450397754762c045f917735b7d2697a4ee608b5",
|
|
22
22
|
"skills/chant-otel-custom-components.md": "82e53bb482281f79b428e66fb80a1c194ead74a1a34a88bbebb11c6ad5493258",
|
|
23
23
|
"skills/chant-otel-platforms.md": "c90836952dc5c88d668b890152a5063bdb656bce73fc7fa3233a3ea58ad85686"
|
|
24
24
|
},
|
|
25
|
-
"composite": "
|
|
25
|
+
"composite": "22fa6fb9f67ca60493659623c02cf1141c9231c7a5c4691a14c72a9e44ecfef2"
|
|
26
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"literal-credential.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/literal-credential.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,4BAA4B,CAAC;AAMxF;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,EAAE,
|
|
1
|
+
{"version":3,"file":"literal-credential.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/literal-credential.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,4BAA4B,CAAC;AAMxF;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,EAAE,QAyEnC,CAAC"}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA2B9D;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,EAAE,aAqFxB,CAAC"}
|
|
@@ -25,25 +25,54 @@ export const literalCredentialRule: LintRule = {
|
|
|
25
25
|
const diagnostics: LintDiagnostic[] = [];
|
|
26
26
|
const source = context.sourceFile;
|
|
27
27
|
|
|
28
|
+
// `const x = { ... }` (or `[...]`, or a string) anywhere in the file, so a
|
|
29
|
+
// config value lifted out of the constructor into a named const, the way
|
|
30
|
+
// COR001 asks, is scanned where it is used.
|
|
31
|
+
const consts = new Map<string, ts.Expression>();
|
|
32
|
+
const collect = (node: ts.Node) => {
|
|
33
|
+
if (ts.isVariableDeclarationList(node) && node.flags & ts.NodeFlags.Const) {
|
|
34
|
+
for (const d of node.declarations) {
|
|
35
|
+
if (ts.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
ts.forEachChild(node, collect);
|
|
39
|
+
};
|
|
40
|
+
collect(source);
|
|
41
|
+
|
|
42
|
+
const flag = (key: string, node: ts.Node) =>
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
ruleId: "OTEL002",
|
|
45
|
+
severity: "error",
|
|
46
|
+
message: `\`${key}\` is a literal credential; write "\${env:NAME}" (or "\${file:/path}") and let the collector read it at start-up`,
|
|
47
|
+
file: context.filePath,
|
|
48
|
+
...position(source, node),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const scanned = new Set<ts.Node>();
|
|
52
|
+
const scanValue = (key: string | undefined, value: ts.Expression) => {
|
|
53
|
+
let init: ts.Expression = value;
|
|
54
|
+
if (ts.isIdentifier(init)) {
|
|
55
|
+
const resolved = consts.get(init.text);
|
|
56
|
+
if (!resolved) return;
|
|
57
|
+
init = resolved;
|
|
58
|
+
}
|
|
59
|
+
if (ts.isObjectLiteralExpression(init) || ts.isArrayLiteralExpression(init)) {
|
|
60
|
+
if (scanned.has(init)) return;
|
|
61
|
+
scanned.add(init);
|
|
62
|
+
if (ts.isObjectLiteralExpression(init)) scan(init);
|
|
63
|
+
else for (const el of init.elements) scanValue(undefined, el as ts.Expression);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!key || !SECRET_KEY.test(key) || /_file$/i.test(key)) return;
|
|
67
|
+
const text = literalText(init);
|
|
68
|
+
if (text === undefined || text === "" || text.includes("${")) return;
|
|
69
|
+
flag(key, value);
|
|
70
|
+
};
|
|
71
|
+
|
|
28
72
|
const scan = (obj: ts.ObjectLiteralExpression) => {
|
|
29
73
|
for (const prop of obj.properties) {
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
const init = prop.initializer;
|
|
33
|
-
if (ts.isObjectLiteralExpression(init)) {
|
|
34
|
-
scan(init);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (!key || !SECRET_KEY.test(key) || /_file$/i.test(key)) continue;
|
|
38
|
-
const text = literalText(init);
|
|
39
|
-
if (text === undefined || text === "" || text.includes("${")) continue;
|
|
40
|
-
diagnostics.push({
|
|
41
|
-
ruleId: "OTEL002",
|
|
42
|
-
severity: "error",
|
|
43
|
-
message: `\`${key}\` is a literal credential; write "\${env:NAME}" (or "\${file:/path}") and let the collector read it at start-up`,
|
|
44
|
-
file: context.filePath,
|
|
45
|
-
...position(source, init),
|
|
46
|
-
});
|
|
74
|
+
if (ts.isShorthandPropertyAssignment(prop)) scanValue(prop.name.text, prop.name);
|
|
75
|
+
else if (ts.isPropertyAssignment(prop)) scanValue(propertyName(prop), prop.initializer);
|
|
47
76
|
}
|
|
48
77
|
};
|
|
49
78
|
|
|
@@ -78,6 +78,17 @@ It deletes prompt, completion, system-instruction and tool-call content from spa
|
|
|
78
78
|
- Every pipeline needs at least one receiver and one exporter (OTEL102). A declared component no pipeline uses is a warning (OTEL103).
|
|
79
79
|
- Declared extensions are enabled in declaration order unless a `Service` lists `extensions` itself.
|
|
80
80
|
|
|
81
|
+
## Starting from an existing config
|
|
82
|
+
|
|
83
|
+
When the user already has a collector config file, import it rather than retyping it:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
chant import otel-collector-config.yaml --output src
|
|
87
|
+
chant build src --lexicon otel -o collector.yaml # the same config back
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The importer writes `receivers.ts`, `processors.ts`, `exporters.ts`, `connectors.ts`, `extensions.ts`, `pipelines.ts`, and `service.ts` when extensions or telemetry need a `Service`. A component type with no built-in class lands in `custom-components.ts` as a `defineComponent` whose config is untyped data; offer to give it a config interface. Settings outside a built-in's config type still import and build, but `tsc` flags them. Read the warnings `chant import` prints: they name anything the lexicon has no place for.
|
|
91
|
+
|
|
81
92
|
## Reading the result
|
|
82
93
|
|
|
83
94
|
`collectorTopologyOf(entities)` returns the pipelines, each component's endpoints and schema pin, for each exporter the signals it carries, the connector `edges` between pipelines, and under `semconv` the semantic-conventions version (`GENAI_SEMCONV_PIN`) the config's `gen_ai.` keys follow, as plain data.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-otel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.0",
|
|
4
4
|
"description": "OpenTelemetry Collector lexicon for chant — typed receivers, processors, exporters, connectors, extensions and pipelines, serialized to collector YAML",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"typescript": "^5.9.3"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@intentius/chant": "^0.
|
|
71
|
+
"@intentius/chant": "^0.98.0",
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,75 @@
|
|
|
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, 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, UPSTREAM_BUILTIN_ONLY } 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
|
+
describe("the generated source type-checks against the lexicon's config types", () => {
|
|
49
|
+
test("for every fixture whose config stays within the typed fields", async () => {
|
|
50
|
+
const projects: Record<string, Array<{ path: string; content: string }>> = {
|
|
51
|
+
gateway: generate(read("gateway.yaml")),
|
|
52
|
+
agent: generate(read("agent-observability-agent.yaml")),
|
|
53
|
+
agentGateway: generate(read("agent-observability-gateway.yaml")),
|
|
54
|
+
genai: generate(collectorYaml(genAiPipeline())),
|
|
55
|
+
everyBuiltin: generate(collectorYaml(everyBuiltin())),
|
|
56
|
+
faultTolerant: generate(read("upstream", "fault-tolerant-logs.yaml")),
|
|
57
|
+
...Object.fromEntries(UPSTREAM_BUILTIN_ONLY.map((f) => [f, generate(read("upstream", f))])),
|
|
58
|
+
...Object.fromEntries((await exampleOutputs()).map(([n, y]) => [n, generate(y)])),
|
|
59
|
+
};
|
|
60
|
+
expect(typeErrors(projects)).toEqual([]);
|
|
61
|
+
}, 120_000);
|
|
62
|
+
|
|
63
|
+
test("a value outside a built-in's typed config is carried as found, and tsc points at it", () => {
|
|
64
|
+
// couchbase uses the filter processor's legacy match syntax and a scrape job's basic_auth,
|
|
65
|
+
// neither of which the lexicon's config types cover.
|
|
66
|
+
const couchbase = typeErrors({ couchbase: generate(read("upstream", "couchbase.yaml")) });
|
|
67
|
+
expect(couchbase).toEqual([
|
|
68
|
+
expect.stringMatching(/^couchbase\/processors\.ts: .*'exclude' does not exist/),
|
|
69
|
+
expect.stringMatching(/^couchbase\/receivers\.ts: .*'basic_auth' does not exist in type 'PrometheusScrapeConfig'/),
|
|
70
|
+
]);
|
|
71
|
+
// The servicegraph test config writes bucket durations as bare integers (nanoseconds); Duration is a string.
|
|
72
|
+
const servicegraph = typeErrors({ servicegraph: generate(read("upstream", "servicegraph-nop.yaml")) });
|
|
73
|
+
expect(servicegraph).toEqual(Array(5).fill("servicegraph/connectors.ts: Type 'number' is not assignable to type 'string'."));
|
|
74
|
+
}, 120_000);
|
|
75
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { CollectorConfig } from "../model";
|
|
3
|
+
import { builtinClassName, generateCollectorFiles, OtelCollectorGenerator, tsLiteral } from "./generator";
|
|
4
|
+
import { OtelCollectorParser } from "./parser";
|
|
5
|
+
|
|
6
|
+
const files = (config: CollectorConfig, meta = {}) =>
|
|
7
|
+
Object.fromEntries(generateCollectorFiles(config, meta).map((f) => [f.path, f.content]));
|
|
8
|
+
|
|
9
|
+
describe("builtinClassName", () => {
|
|
10
|
+
test("maps every built-in kind and type to its exported class", () => {
|
|
11
|
+
expect(builtinClassName("receiver", "otlp")).toBe("OtlpReceiver");
|
|
12
|
+
expect(builtinClassName("exporter", "otlp")).toBe("OtlpExporter");
|
|
13
|
+
expect(builtinClassName("processor", "tail_sampling")).toBe("TailSamplingProcessor");
|
|
14
|
+
expect(builtinClassName("connector", "spanmetrics")).toBe("SpanMetricsConnector");
|
|
15
|
+
expect(builtinClassName("connector", "sum")).toBe("SumConnector");
|
|
16
|
+
expect(builtinClassName("receiver", "k8s_cluster")).toBe("K8sClusterReceiver");
|
|
17
|
+
expect(builtinClassName("extension", "health_check")).toBe("HealthCheckExtension");
|
|
18
|
+
expect(builtinClassName("exporter", "datadog")).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("tsLiteral", () => {
|
|
23
|
+
test("writes plain keys bare, quotes the rest, and prefers single quotes for OTTL", () => {
|
|
24
|
+
expect(tsLiteral({ a: 1, "b.c": true, d: null }, 0)).toBe('{ a: 1, "b.c": true, d: null }');
|
|
25
|
+
expect(tsLiteral('attributes["x"] == "y"', 0)).toBe(`'attributes["x"] == "y"'`);
|
|
26
|
+
expect(tsLiteral(`it's "both"`, 0)).toBe(JSON.stringify(`it's "both"`));
|
|
27
|
+
expect(tsLiteral("${env:X}", 0)).toBe('"${env:X}"');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("breaks a value across lines past 100 columns", () => {
|
|
31
|
+
const out = tsLiteral({ list: Array.from({ length: 12 }, (_, i) => `item-${i}`) }, 0);
|
|
32
|
+
expect(out.split("\n").length).toBeGreaterThan(1);
|
|
33
|
+
expect(out.split("\n").every((l) => l.length <= 100)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("generateCollectorFiles", () => {
|
|
38
|
+
test("one module per section, pipelines importing what they reference", () => {
|
|
39
|
+
const out = files({
|
|
40
|
+
receivers: { otlp: { protocols: { grpc: null } } },
|
|
41
|
+
processors: { batch: null },
|
|
42
|
+
exporters: { "otlp/tempo": { endpoint: "tempo:4317" } },
|
|
43
|
+
service: { pipelines: { traces: { receivers: ["otlp"], processors: ["batch"], exporters: ["otlp/tempo"] } } },
|
|
44
|
+
});
|
|
45
|
+
expect(Object.keys(out)).toEqual(["receivers.ts", "processors.ts", "exporters.ts", "pipelines.ts"]);
|
|
46
|
+
expect(out["receivers.ts"]).toContain('const otlpProtocols: OtlpReceiverConfig["protocols"] = { grpc: null };');
|
|
47
|
+
expect(out["receivers.ts"]).toContain("const otlp = new OtlpReceiver({ protocols: otlpProtocols });");
|
|
48
|
+
expect(out["receivers.ts"]).toContain("export { otlp };");
|
|
49
|
+
expect(out["processors.ts"]).toContain("const batch = new BatchProcessor();");
|
|
50
|
+
expect(out["exporters.ts"]).toContain('const otlpTempo = new OtlpExporter({ name: "tempo", endpoint: "tempo:4317" });');
|
|
51
|
+
expect(out["pipelines.ts"]).toContain('import { otlpTempo } from "./exporters";');
|
|
52
|
+
expect(out["pipelines.ts"]).toContain(
|
|
53
|
+
[
|
|
54
|
+
"const traces = new Pipeline({",
|
|
55
|
+
' signal: "traces",',
|
|
56
|
+
" receivers: [otlp],",
|
|
57
|
+
" processors: [batch],",
|
|
58
|
+
" exporters: [otlpTempo],",
|
|
59
|
+
"});",
|
|
60
|
+
].join("\n"),
|
|
61
|
+
);
|
|
62
|
+
expect(out["pipelines.ts"]).toContain("export { traces };");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("an id shared across kinds gets a kind suffix; a connector is one constant on both sides", () => {
|
|
66
|
+
const out = files({
|
|
67
|
+
receivers: { otlp: {}, prometheus: { config: { scrape_configs: [] } } },
|
|
68
|
+
exporters: { otlp: { endpoint: "x:4317" }, prometheus: { endpoint: "0.0.0.0:8889" } },
|
|
69
|
+
connectors: { spanmetrics: {} },
|
|
70
|
+
service: {
|
|
71
|
+
pipelines: {
|
|
72
|
+
traces: { receivers: ["otlp"], exporters: ["otlp", "spanmetrics"] },
|
|
73
|
+
metrics: { receivers: ["prometheus", "spanmetrics"], exporters: ["prometheus"] },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
expect(out["receivers.ts"]).toContain("const otlpReceiver = new OtlpReceiver();");
|
|
78
|
+
expect(out["exporters.ts"]).toContain('const otlpExporter = new OtlpExporter({ endpoint: "x:4317" });');
|
|
79
|
+
expect(out["pipelines.ts"]).toContain(" receivers: [otlpReceiver],\n exporters: [otlpExporter, spanmetrics],");
|
|
80
|
+
expect(out["pipelines.ts"]).toContain(" receivers: [prometheusReceiver, spanmetrics],\n exporters: [prometheusExporter],");
|
|
81
|
+
expect(out["pipelines.ts"]).toContain('import { spanmetrics } from "./connectors";');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a component type chant does not ship is defined once with defineComponent and COLLECTOR_PIN", () => {
|
|
85
|
+
const out = files({
|
|
86
|
+
exporters: { "datadog/a": { api: { key: "${env:DD}" } }, "datadog/b": {}, otlp_http: {} },
|
|
87
|
+
service: { pipelines: { logs: { receivers: [], exporters: ["datadog/a", "datadog/b", "otlp_http"] } } },
|
|
88
|
+
});
|
|
89
|
+
const custom = out["custom-components.ts"];
|
|
90
|
+
expect(custom).toContain('import { COLLECTOR_PIN, defineComponent } from "@intentius/chant-lexicon-otel";');
|
|
91
|
+
expect(custom.match(/defineComponent<Record<string, unknown>>\(\)/g)).toHaveLength(2);
|
|
92
|
+
expect(custom).toContain('const DatadogExporter = defineComponent<Record<string, unknown>>()({\n kind: "exporter",\n type: "datadog",\n pin: COLLECTOR_PIN,\n});');
|
|
93
|
+
// `OtlpHttpExporter` is the built-in otlphttp exporter's class, so this one is numbered.
|
|
94
|
+
expect(custom).toContain("const OtlpHttpExporter2 = defineComponent");
|
|
95
|
+
expect(custom).toContain("export { DatadogExporter, OtlpHttpExporter2 };");
|
|
96
|
+
expect(out["exporters.ts"]).toContain('import { DatadogExporter, OtlpHttpExporter2 } from "./custom-components";');
|
|
97
|
+
expect(out["exporters.ts"]).toContain("const datadogAApi = { key: \"${env:DD}\" };");
|
|
98
|
+
expect(out["exporters.ts"]).toContain('const datadogA = new DatadogExporter({ name: "a", api: datadogAApi });');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a pin named by the header replaces COLLECTOR_PIN", () => {
|
|
102
|
+
const out = files(
|
|
103
|
+
{ exporters: { "acme/x": {} } },
|
|
104
|
+
{ pins: [{ kind: "exporter", id: "acme/x", pin: { source: "@acme/otel", version: "2.0.0", digest: "sha256:1" } }] },
|
|
105
|
+
);
|
|
106
|
+
expect(out["custom-components.ts"]).toContain('pin: { source: "@acme/otel", version: "2.0.0", digest: "sha256:1" },');
|
|
107
|
+
expect(out["custom-components.ts"]).not.toContain("COLLECTOR_PIN");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a Service only when extensions or telemetry differ from the default", () => {
|
|
111
|
+
const ext = { health_check: null, zpages: null };
|
|
112
|
+
expect(files({ extensions: ext, service: { extensions: ["health_check", "zpages"] } })["service.ts"]).toBeUndefined();
|
|
113
|
+
expect(files({ extensions: ext, service: { extensions: ["zpages", "health_check"] } })["service.ts"]).toContain(
|
|
114
|
+
"const service = new Service({ extensions: [zpages, healthCheck] });",
|
|
115
|
+
);
|
|
116
|
+
// Declared but not enabled: an empty list, so the rebuilt config enables none either.
|
|
117
|
+
expect(files({ extensions: ext })["service.ts"]).toContain("const service = new Service({ extensions: [] });");
|
|
118
|
+
const withTelemetry = files({ service: { telemetry: { logs: { level: "warn" } } } })["service.ts"];
|
|
119
|
+
expect(withTelemetry).toContain('const telemetry: ServiceTelemetry = { logs: { level: "warn" } };');
|
|
120
|
+
expect(withTelemetry).toContain("const service = new Service({ telemetry });");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("more than eight declarables of one section split into numbered modules", () => {
|
|
124
|
+
const processors = Object.fromEntries(Array.from({ length: 10 }, (_, i) => [`batch/b${i}`, null]));
|
|
125
|
+
const out = files({ processors });
|
|
126
|
+
expect(Object.keys(out)).toEqual(["processors-1.ts", "processors-2.ts"]);
|
|
127
|
+
expect(out["processors-1.ts"].match(/new BatchProcessor/g)).toHaveLength(8);
|
|
128
|
+
expect(out["processors-2.ts"].match(/new BatchProcessor/g)).toHaveLength(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("an id nothing declares stays a string; a signal Pipeline does not type is suppressed with a reason", () => {
|
|
132
|
+
const out = files({ service: { pipelines: { "profiles/x": { receivers: ["otlp"], exporters: ["debug"] } } } });
|
|
133
|
+
expect(out["pipelines.ts"]).toContain(
|
|
134
|
+
[
|
|
135
|
+
"const profilesX = new Pipeline({",
|
|
136
|
+
' // @ts-expect-error "profiles" is a collector signal Pipeline does not type',
|
|
137
|
+
' signal: "profiles",',
|
|
138
|
+
' name: "x",',
|
|
139
|
+
' receivers: ["otlp"],',
|
|
140
|
+
' exporters: ["debug"],',
|
|
141
|
+
"});",
|
|
142
|
+
].join("\n"),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("names are valid identifiers even from awkward ids", () => {
|
|
147
|
+
const out = files({
|
|
148
|
+
exporters: { "otlp/2": { endpoint: "a:1" }, "otlp/my-backend.eu": { endpoint: "b:1" }, "debug/new": {} },
|
|
149
|
+
service: { pipelines: { "traces/default": { receivers: [], exporters: ["otlp/2"] } } },
|
|
150
|
+
});
|
|
151
|
+
expect(out["exporters.ts"]).toContain("const otlp2 = new OtlpExporter(");
|
|
152
|
+
expect(out["exporters.ts"]).toContain("const otlpMyBackendEu = new OtlpExporter(");
|
|
153
|
+
expect(out["exporters.ts"]).toContain("const debugNew = new DebugExporter(");
|
|
154
|
+
expect(out["pipelines.ts"]).toContain("const tracesDefault = new Pipeline(");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("OtelCollectorGenerator", () => {
|
|
159
|
+
test("generates from the parser's IR, and returns a file for an empty config", () => {
|
|
160
|
+
const gen = new OtelCollectorGenerator();
|
|
161
|
+
const ir = new OtelCollectorParser().parse("receivers:\n otlp: {}\n");
|
|
162
|
+
expect(gen.generate(ir).map((f) => f.path)).toEqual(["receivers.ts"]);
|
|
163
|
+
expect(gen.generate({ resources: [], parameters: [] })).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
});
|