From b3f6a008b6c724e92143927ef4510267c2e5f9d5 Mon Sep 17 00:00:00 2001 From: Ben Blackmore Date: Mon, 20 Nov 2023 21:22:44 +0100 Subject: [PATCH] feat: get started with recipes --- packages/otelbin-recipes/.env.example | 1 + packages/otelbin-recipes/.gitignore | 133 +++++++++ packages/otelbin-recipes/README.md | 14 + packages/otelbin-recipes/package-lock.json | 302 +++++++++++++++++++++ packages/otelbin-recipes/package.json | 22 ++ packages/otelbin-recipes/recipes/test.yaml | 39 +++ packages/otelbin-recipes/src/index.ts | 23 ++ packages/otelbin-recipes/src/recipes.ts | 28 ++ packages/otelbin-recipes/src/validation.ts | 114 ++++++++ packages/otelbin-recipes/tsconfig.json | 34 +++ 10 files changed, 710 insertions(+) create mode 100644 packages/otelbin-recipes/.env.example create mode 100644 packages/otelbin-recipes/.gitignore create mode 100644 packages/otelbin-recipes/README.md create mode 100644 packages/otelbin-recipes/package-lock.json create mode 100644 packages/otelbin-recipes/package.json create mode 100644 packages/otelbin-recipes/recipes/test.yaml create mode 100644 packages/otelbin-recipes/src/index.ts create mode 100644 packages/otelbin-recipes/src/recipes.ts create mode 100644 packages/otelbin-recipes/src/validation.ts create mode 100644 packages/otelbin-recipes/tsconfig.json diff --git a/packages/otelbin-recipes/.env.example b/packages/otelbin-recipes/.env.example new file mode 100644 index 00000000..3da4a156 --- /dev/null +++ b/packages/otelbin-recipes/.env.example @@ -0,0 +1 @@ +API_ORIGIN=https://www.otelbin.io diff --git a/packages/otelbin-recipes/.gitignore b/packages/otelbin-recipes/.gitignore new file mode 100644 index 00000000..98930cbe --- /dev/null +++ b/packages/otelbin-recipes/.gitignore @@ -0,0 +1,133 @@ +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +result.json diff --git a/packages/otelbin-recipes/README.md b/packages/otelbin-recipes/README.md new file mode 100644 index 00000000..378d92a9 --- /dev/null +++ b/packages/otelbin-recipes/README.md @@ -0,0 +1,14 @@ +# OTelBin Recipes + +OTelBin supports common collector configuration recipes that users can leverage to get started quickly. + +## Contributing a Recipe + +Add a yaml file to the `recipes/` directory + +TODO + +## Generating a Compatibility Matrix + +1. Make sure you have all required configuration `cp .env.example .env` +2. Generate the compatibility matrix `npm run start` diff --git a/packages/otelbin-recipes/package-lock.json b/packages/otelbin-recipes/package-lock.json new file mode 100644 index 00000000..c4a05a3d --- /dev/null +++ b/packages/otelbin-recipes/package-lock.json @@ -0,0 +1,302 @@ +{ + "name": "otelbin-recipes", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "otelbin-recipes", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@types/cli-progress": "^3.11.5", + "@types/js-yaml": "^4.0.9", + "cli-progress": "^3.12.0", + "dotenv": "^16.3.1", + "js-yaml": "^4.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.3.2", + "zod": "^3.22.4" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/cli-progress": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", + "integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, + "node_modules/@types/node": { + "version": "20.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", + "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/otelbin-recipes/package.json b/packages/otelbin-recipes/package.json new file mode 100644 index 00000000..a838fea7 --- /dev/null +++ b/packages/otelbin-recipes/package.json @@ -0,0 +1,22 @@ +{ + "name": "otelbin-recipes", + "version": "1.0.0", + "description": "Recipes for OTelBin including configuration validation", + "main": "src/index.ts", + "scripts": { + "start": "ts-node src/index.ts" + }, + "author": "Dash0 Inc.", + "license": "Apache-2.0", + "private": true, + "dependencies": { + "@types/cli-progress": "^3.11.5", + "@types/js-yaml": "^4.0.9", + "cli-progress": "^3.12.0", + "dotenv": "^16.3.1", + "js-yaml": "^4.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.3.2", + "zod": "^3.22.4" + } +} diff --git a/packages/otelbin-recipes/recipes/test.yaml b/packages/otelbin-recipes/recipes/test.yaml new file mode 100644 index 00000000..3da6e669 --- /dev/null +++ b/packages/otelbin-recipes/recipes/test.yaml @@ -0,0 +1,39 @@ +title: 'Foo' +author: 'Bar' +description: 'Hello World' +config: | + # Learn more about the OpenTelemetry Collector via + # https://opentelemetry.io/docs/collector/ + receivers: + otlp: + protocols: + grpc: + http: + + processors: + batch: + + exporters: + otlp: + endpoint: otelcol:4317 + + extensions: + health_check: + pprof: + zpages: + + service: + extensions: [ health_check, pprof, zpages ] + pipelines: + traces: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ otlp ] + metrics: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ otlp ] + logs: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ otlp ] diff --git a/packages/otelbin-recipes/src/index.ts b/packages/otelbin-recipes/src/index.ts new file mode 100644 index 00000000..c6f0abc5 --- /dev/null +++ b/packages/otelbin-recipes/src/index.ts @@ -0,0 +1,23 @@ +import "dotenv/config"; + +import { getRecipes } from "./recipes"; +import { validateRecipes } from "./validation"; +import fs from "fs/promises"; +import path from "path"; + +main() + .catch(e => { + console.error("Uncaught error in main()", e); + process.exit(1); + }); + +async function main() { + console.info('Collecting recipes'); + const recipes = await getRecipes(); + console.info(`Found %d recipes`, recipes.length) + + const validatedRecipes = await validateRecipes(recipes); + + await fs.writeFile(path.join(__dirname, '..', 'result.json'), JSON.stringify(validatedRecipes)) + console.log('Output written to result.json'); +} diff --git a/packages/otelbin-recipes/src/recipes.ts b/packages/otelbin-recipes/src/recipes.ts new file mode 100644 index 00000000..359ca7f1 --- /dev/null +++ b/packages/otelbin-recipes/src/recipes.ts @@ -0,0 +1,28 @@ +import z from "zod"; +import fs from "fs/promises"; +import path from "path"; +import { load } from "js-yaml"; + +export const recipeSchema = z.object({ + title: z.string(), + author: z.string(), + description: z.string(), + // We keep the config as a string so that recipe authors can embed comments. + config: z.string() +}) +export type Recipe = z.infer + +export async function getRecipes(): Promise { + const directoryEntries = await fs.readdir(path.join(__dirname, '..', 'recipes'), { + withFileTypes: true + }); + + return Promise.all(directoryEntries + .filter(entry => entry.isFile()) + .map(entry => getRecipe(path.join(entry.path, entry.name)))); +} + +async function getRecipe(absolutePath: string): Promise { + const fileContent = await fs.readFile(absolutePath, {encoding: 'utf8'}); + return recipeSchema.parse(load(fileContent)); +} diff --git a/packages/otelbin-recipes/src/validation.ts b/packages/otelbin-recipes/src/validation.ts new file mode 100644 index 00000000..fa685dd5 --- /dev/null +++ b/packages/otelbin-recipes/src/validation.ts @@ -0,0 +1,114 @@ +import z from "zod"; +import { Recipe } from "./recipes"; +import cliProgress from "cli-progress"; + +type ValidationResult = "valid" | "invalid" | "unknown"; + +interface DistributionCompatibility { + // Short distribution identifies as returned by the support-distribution endpoint. + // Sample: otelcol-core, otelcol-contrib and adot + distribution: string; + // The version of the distribution. + version: string; + result: ValidationResult; + // The error returned by the validation endpoint – if any. + error?: string; +} + +export interface RecipeWithDistributionCompatibility extends Recipe { + compatibility: DistributionCompatibility[]; +} + +export async function validateRecipes(recipes: Recipe[]): Promise { + console.info("Retrieving supported distributions"); + const supportedDistributions = await getSupportedDistributions(); + const uniqueDistributionAndVersionCount = getUniqueDistributionAndVersionCount(supportedDistributions); + console.info(`Retrieved ${Object.keys(supportedDistributions).length} supported distributions. In total ${uniqueDistributionAndVersionCount} distro/version permutations.`); + + console.info('Starting validation of recipes.'); + const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); + try { + let numberOfExecutedValidationCalls = 0; + progressBar.start(recipes.length * uniqueDistributionAndVersionCount, numberOfExecutedValidationCalls); + + const result: RecipeWithDistributionCompatibility[] = []; + + + // We could do a combination of Promise.all(recipes.map(...)) to run this + // fully asynchronously, but this would just result in us immediately + // hitting the rate limit and then running into timeouts/rate limiting failures. + // So we do this in series. This also has the benefit that we can leverage + // the server-side lease acquisition timeout. + // + // An alternative would do use a Bluebird like Promise.map(promises, {concurrency: 5}), + // but this is not worth another dependency and the complexity that this brings to + // this system. + for (const recipe of recipes) { + const compatibilities: DistributionCompatibility[] = []; + + for (const [distributionName, { releases }] of Object.entries(supportedDistributions)) { + for (const { version: distributionVersion } of releases) { + const compatibility = await validateRecipe(recipe, distributionName, distributionVersion); + compatibilities.push(compatibility); + numberOfExecutedValidationCalls++; + progressBar.update(numberOfExecutedValidationCalls) + } + } + result.push({ + ...recipe, + compatibility: compatibilities + }); + } + + return result; + } finally { + progressBar.stop(); + } +} + +const supportedDistributionsSchema = z.record(z.object({ + releases: z.array(z.object({ + version: z.string() + })) +})); +type SupportedDistributions = z.infer; + +async function getSupportedDistributions(): Promise { + const response = await fetch(`${process.env.API_ORIGIN}/validation/supported-distributions`); + if (!response.ok) { + throw new Error("Failed to retrieve list of supported distributions"); + } + const body = await response.json(); + return supportedDistributionsSchema.parse(body); +} + +async function validateRecipe(recipe: Recipe, distribution: string, version: string): Promise { + const response = await fetch(`${process.env.API_ORIGIN}/validation?distro=${encodeURIComponent(distribution)}&version=${encodeURIComponent(version)}`, { + method: "POST", + body: recipe.config + }); + + if (!response.ok) { + throw new Error(`Failed to validate distribution configuration for ${distribution} ${version}`); + } + + let result: ValidationResult = "valid"; + let error: string | undefined = undefined; + const body = await response.json() as any; + if ("error" in body && typeof body.error==="string") { + result = "invalid"; + error = body.error; + } + + return { + distribution, + version, + result, + error + }; +} + +function getUniqueDistributionAndVersionCount(distributions: SupportedDistributions): number { + return Object.values(distributions) + .reduce((agg, distribution) => agg + distribution.releases.length, 0) +} diff --git a/packages/otelbin-recipes/tsconfig.json b/packages/otelbin-recipes/tsconfig.json new file mode 100644 index 00000000..60f0fa4e --- /dev/null +++ b/packages/otelbin-recipes/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2019" + ], + "module": "CommonJS", + "noEmitOnError": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2019" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + ] +}