@ogcio/o11y-validator 0.2.1
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 +65 -0
- package/bin/o11y-validator.mjs +79 -0
- package/lib/init.mjs +55 -0
- package/lib/rules.mjs +83 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @ogcio/o11y-validator
|
|
2
|
+
|
|
3
|
+
npm wrapper for the o11y-validator Go binary. Allows running the validator via `npx` without installing Go.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Build the binary for your current platform
|
|
9
|
+
pnpm build:local
|
|
10
|
+
|
|
11
|
+
# Run directly
|
|
12
|
+
node bin/o11y-validator.mjs
|
|
13
|
+
|
|
14
|
+
# Or via npx (after publishing)
|
|
15
|
+
npx @ogcio/o11y-validator
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Environment variables
|
|
19
|
+
|
|
20
|
+
The launcher reads a `.env` file from the current working directory. Shell env vars take precedence.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# .env
|
|
24
|
+
RULES_DIR=./rules
|
|
25
|
+
FORWARD_TO_COLLECTOR=false
|
|
26
|
+
LOG_LEVEL=debug
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
The validator needs a rules directory. If you do **not** set `RULES_DIR`, the
|
|
32
|
+
launcher downloads the rule set from GitHub at the tag matching this package's
|
|
33
|
+
version and caches it **inside the installed package** (`.rules-cache/`, scoped
|
|
34
|
+
to the package version). Subsequent runs reuse the cache — no network needed. If
|
|
35
|
+
the rules cannot be obtained (and `RULES_DIR` is unset), the launcher **exits
|
|
36
|
+
with an error**.
|
|
37
|
+
|
|
38
|
+
The repo and ref are taken from `package.json` (`repository` and
|
|
39
|
+
`name@version`). The path to the rules inside the repo is **hardcoded** in the
|
|
40
|
+
launcher as `o11y-validator/rules` — see the maintainer note below.
|
|
41
|
+
|
|
42
|
+
| Variable | Default | Purpose |
|
|
43
|
+
| -------------- | --------- | ---------------------------------------------------------- |
|
|
44
|
+
| `RULES_DIR` | _(unset)_ | Use a local rules directory and skip downloading entirely. |
|
|
45
|
+
| `GITHUB_TOKEN` | _(unset)_ | Raises rate limits; required if the repo is private. |
|
|
46
|
+
|
|
47
|
+
> **Maintainer note:** if the rules directory is relocated in a future release,
|
|
48
|
+
> update `RULES_SUBPATH` in `bin/o11y-validator.mjs` to match. It is intentionally
|
|
49
|
+
> not configurable via env.
|
|
50
|
+
|
|
51
|
+
## Build for all platforms
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Cross-compiles every target with plain `go build` CGO disabled,
|
|
58
|
+
so no zig, osxcross, or macOS agent is required, `darwin/arm64` is ad-hoc signed
|
|
59
|
+
by the Go linker automatically.
|
|
60
|
+
|
|
61
|
+
## Releasing
|
|
62
|
+
|
|
63
|
+
A pre-commit hook keeps `o11y-validator.lock` in sync with the `o11y-validator/`
|
|
64
|
+
tree so validator changes can trigger a release of this package.
|
|
65
|
+
see [distribution.md](../../o11y-validator/docs/distribution.md).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ogcio/o11y-validator - launcher
|
|
3
|
+
// 1 - Loads .env
|
|
4
|
+
// 2 - dispatches the `init` command or resolves the platform binary and executes it with inherited stdio.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { arch, platform } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { ensureRulesCache, RULES_REF } from "../lib/rules.mjs";
|
|
12
|
+
import { initCommand } from "../lib/init.mjs";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
// ── Load .env file from cwd (does NOT override existing env vars) ──
|
|
17
|
+
const envFile = join(process.cwd(), ".env");
|
|
18
|
+
if (existsSync(envFile)) {
|
|
19
|
+
for (const line of readFileSync(envFile, "utf8").split("\n")) {
|
|
20
|
+
const match = line.match(/^\s*([^#=]+?)\s*=\s*(.*)\s*$/);
|
|
21
|
+
if (match && !process.env[match[1]]) {
|
|
22
|
+
process.env[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
|
|
29
|
+
// `init <path>` — eject the version-pinned rules into a directory
|
|
30
|
+
if (args[0] === "init") {
|
|
31
|
+
try {
|
|
32
|
+
await initCommand(args[1]);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`[o11y-validator] init failed: ${err.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Provide validation rules for the run
|
|
41
|
+
// A RULES_DIR set by the user (env or .env) skips fetching entirely.
|
|
42
|
+
if (!process.env.RULES_DIR) {
|
|
43
|
+
try {
|
|
44
|
+
process.env.RULES_DIR = await ensureRulesCache();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`[o11y-validator] could not obtain rules (${RULES_REF}): ${err.message}`);
|
|
47
|
+
console.error(
|
|
48
|
+
`[o11y-validator] set RULES_DIR to a local rules directory, or check your network / GITHUB_TOKEN.`,
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Resolve platform binary ──
|
|
55
|
+
const normalizedArch = arch() === "x64" ? "x64" : "arm64";
|
|
56
|
+
const bin = `o11y-validator-${platform()}-${normalizedArch}`;
|
|
57
|
+
const binPath = join(__dirname, bin);
|
|
58
|
+
|
|
59
|
+
if (!existsSync(binPath)) {
|
|
60
|
+
console.error(
|
|
61
|
+
`[o11y-validator] No binary found for platform: ${platform()}-${normalizedArch}`,
|
|
62
|
+
);
|
|
63
|
+
console.error(`Expected binary at: ${binPath}`);
|
|
64
|
+
console.error(
|
|
65
|
+
`\nAvailable platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64`,
|
|
66
|
+
);
|
|
67
|
+
console.error(`Run "pnpm build" in packages/validator-cli to compile.`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Execute the Go binary ──
|
|
72
|
+
try {
|
|
73
|
+
execFileSync(binPath, args, {
|
|
74
|
+
stdio: "inherit",
|
|
75
|
+
env: process.env,
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
process.exit(err.status ?? 1);
|
|
79
|
+
}
|
package/lib/init.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @ogcio/o11y-validator - `init` command
|
|
2
|
+
// Ejects the version-pinned rule set into a user-provided directory so teams can
|
|
3
|
+
// review or customize the rules locally. Fails on:
|
|
4
|
+
// - missing path,
|
|
5
|
+
// - non-empty destination,
|
|
6
|
+
// - unobtainable rules.
|
|
7
|
+
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { ensureRulesCache, listRuleFiles, pkg, RULES_REF } from "./rules.mjs";
|
|
11
|
+
|
|
12
|
+
export async function initCommand(targetArg) {
|
|
13
|
+
if (!targetArg) {
|
|
14
|
+
throw new Error("usage: o11y-validator init <path>");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const target = resolve(process.cwd(), targetArg);
|
|
18
|
+
|
|
19
|
+
if (existsSync(target)) {
|
|
20
|
+
if (!statSync(target).isDirectory()) {
|
|
21
|
+
throw new Error(`destination exists and is not a directory: ${target}`);
|
|
22
|
+
}
|
|
23
|
+
if (readdirSync(target).length > 0) {
|
|
24
|
+
throw new Error(`destination directory is not empty: ${target}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Source the rules from the git repo cache (download if never fetched).
|
|
29
|
+
let cacheDir;
|
|
30
|
+
try {
|
|
31
|
+
cacheDir = await ensureRulesCache();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new Error(`could not obtain rules (${RULES_REF}): ${err.message}`, {
|
|
34
|
+
cause: err,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const files = listRuleFiles(cacheDir);
|
|
39
|
+
if (files.length === 0) {
|
|
40
|
+
throw new Error(`no rule files available for ${RULES_REF}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mkdirSync(target, { recursive: true });
|
|
44
|
+
for (const name of files) {
|
|
45
|
+
copyFileSync(join(cacheDir, name), join(target, name));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`[o11y-validator] Initialized rules in ${target}`);
|
|
49
|
+
console.log(`[o11y-validator] package version: ${pkg.version}`);
|
|
50
|
+
console.log(`[o11y-validator] rules ref: ${RULES_REF}`);
|
|
51
|
+
console.log(`[o11y-validator] files: ${files.length}`);
|
|
52
|
+
console.log(`[o11y-validator] Use them with env: RULES_DIR=${target}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
package/lib/rules.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @ogcio/o11y-validator - rules resolution
|
|
2
|
+
// Rules are fetched from GitHub at the tag matching this package's version and cached on disk,
|
|
3
|
+
// so a given release always resolves the rule set it shipped with.
|
|
4
|
+
// The repo and ref come from package.json;
|
|
5
|
+
// The path within the repo is hardcoded below.
|
|
6
|
+
//
|
|
7
|
+
// NOTE: if the rules directory is relocated in a future release, update
|
|
8
|
+
// RULES_SUBPATH to match.
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
|
|
16
|
+
export const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
17
|
+
export const RULES_SUBPATH = "o11y-validator/rules";
|
|
18
|
+
export const RULES_REF = `${pkg.name}@${pkg.version}`;
|
|
19
|
+
|
|
20
|
+
export function rulesCacheDir() {
|
|
21
|
+
// Cache inside the installed package, alongside the binary, scoped to the version.
|
|
22
|
+
return join(pkgRoot, ".rules-cache");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function listRuleFiles(dir) {
|
|
26
|
+
if (!existsSync(dir)) return [];
|
|
27
|
+
return readdirSync(dir)
|
|
28
|
+
.filter((name) => /\.ya?ml$/.test(name))
|
|
29
|
+
.sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ghRepo() {
|
|
33
|
+
const repository = pkg.repository;
|
|
34
|
+
const url = typeof repository === "string" ? repository : repository?.url || "";
|
|
35
|
+
const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?/i);
|
|
36
|
+
if (!match) {
|
|
37
|
+
throw new Error("package.json 'repository' must be a GitHub URL");
|
|
38
|
+
}
|
|
39
|
+
return `${match[1]}/${match[2]}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ghHeaders(accept = "application/vnd.github+json") {
|
|
43
|
+
const headers = { "user-agent": "o11y-validator-cli", accept };
|
|
44
|
+
// GITHUB_TOKEN required for private repos.
|
|
45
|
+
const token = process.env.GITHUB_TOKEN || process.env.NODE_AUTH_TOKEN;
|
|
46
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
47
|
+
return headers;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function downloadRules(destDir) {
|
|
51
|
+
const repo = ghRepo();
|
|
52
|
+
const listUrl = `https://api.github.com/repos/${repo}/contents/${RULES_SUBPATH}?ref=${encodeURIComponent(RULES_REF)}`;
|
|
53
|
+
const res = await fetch(listUrl, { headers: ghHeaders() });
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new Error(`GitHub contents ${res.status} for ref ${RULES_REF}`);
|
|
56
|
+
}
|
|
57
|
+
const entries = await res.json();
|
|
58
|
+
const files = entries.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name));
|
|
59
|
+
if (files.length === 0) {
|
|
60
|
+
throw new Error(`no rule files at ${RULES_SUBPATH}@${RULES_REF}`);
|
|
61
|
+
}
|
|
62
|
+
mkdirSync(destDir, { recursive: true });
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
// Fetch raw bytes via the contents API so the auth header also works for
|
|
65
|
+
// private repositories.
|
|
66
|
+
const raw = await fetch(file.url, {
|
|
67
|
+
headers: ghHeaders("application/vnd.github.raw"),
|
|
68
|
+
});
|
|
69
|
+
if (!raw.ok) throw new Error(`download ${file.name}: ${raw.status}`);
|
|
70
|
+
writeFileSync(join(destDir, file.name), Buffer.from(await raw.arrayBuffer()));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Ensure the rules cache is populated, downloading it if empty. Returns the
|
|
75
|
+
// cache directory. Throws if rules cannot be obtained.
|
|
76
|
+
export async function ensureRulesCache() {
|
|
77
|
+
const dir = rulesCacheDir();
|
|
78
|
+
if (listRuleFiles(dir).length === 0) {
|
|
79
|
+
await downloadRules(dir);
|
|
80
|
+
}
|
|
81
|
+
return dir;
|
|
82
|
+
}
|
|
83
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ogcio/o11y-validator",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Telemetry rules validator for OGCIO services — wraps the Go binary for npx execution",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ogcio/o11y.git"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"o11y-validator": "./bin/o11y-validator.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"lib/"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"opentelemetry",
|
|
22
|
+
"observability",
|
|
23
|
+
"validator",
|
|
24
|
+
"ottl"
|
|
25
|
+
],
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "./scripts/build.sh",
|
|
29
|
+
"build:local": "./scripts/build.sh local",
|
|
30
|
+
"lock": "./scripts/update-lock.sh"
|
|
31
|
+
}
|
|
32
|
+
}
|