diff --git a/.gitignore b/.gitignore index ad67955..1332ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +demos/otel-benchmark/.forge/ diff --git a/Cargo.lock b/Cargo.lock index 16163b8..054aa96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,18 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.18.0" @@ -232,6 +244,155 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "aws-sigv4" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.5.0", + "percent-encoding", + "sha2 0.11.0", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954c563ce84507722d2679f07a35d21b9c6466b3872d513020d0281fc8112ac9" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-types" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce83ce9abbb198d25bc7131e468d0f9fe1257125e58c39f3f9fc9f5098c9647" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -244,6 +405,16 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -342,6 +513,16 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "camino" version = "1.2.5" @@ -587,6 +768,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.20" @@ -1110,7 +1300,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.5.0", "indexmap 2.14.0", "slab", "tokio", @@ -1209,6 +1399,17 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.5.0" @@ -1219,6 +1420,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.1.0" @@ -1226,7 +1438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http", + "http 1.5.0", ] [[package]] @@ -1237,8 +1449,8 @@ checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.5.0", + "http-body 1.1.0", "pin-project-lite", ] @@ -1273,9 +1485,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http", - "http-body", + "h2", + "http 1.5.0", + "http-body 1.1.0", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1289,7 +1503,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http", + "http 1.5.0", "hyper", "hyper-util", "rustls", @@ -1298,6 +1512,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1308,8 +1535,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", + "http 1.5.0", + "http-body 1.1.0", "hyper", "ipnet", "libc", @@ -1512,6 +1739,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1722,6 +1958,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.11.0" @@ -1789,6 +2031,12 @@ dependencies = [ "sketches-ddsketch", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1959,6 +2207,82 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.20", +] + +[[package]] +name = "opentelemetry-http" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" +dependencies = [ + "async-trait", + "bytes", + "http 1.5.0", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" +dependencies = [ + "http 1.5.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.20", + "tokio", + "tonic", + "tonic-types", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "portable-atomic", + "rand 0.9.5", + "thiserror 2.0.20", + "tokio", + "tokio-stream", +] + [[package]] name = "ouroboros" version = "0.18.5" @@ -1983,6 +2307,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" @@ -2028,12 +2358,38 @@ dependencies = [ "indexmap 2.14.0", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.34" @@ -2073,14 +2429,14 @@ dependencies = [ [[package]] name = "praxis-ai-apis" version = "0.2.0" -source = "git+https://github.com/praxis-proxy/ai?rev=45c075190c646c4e0397b29df9d944b7e49ed039#45c075190c646c4e0397b29df9d944b7e49ed039" +source = "git+https://github.com/praxis-proxy/ai?rev=5d6a4e822969ef4570d9e0cf4423087700b11136#5d6a4e822969ef4570d9e0cf4423087700b11136" dependencies = [ "async-trait", "base64 0.23.1", "bytes", "dashmap 6.2.1", "futures", - "http", + "http 1.5.0", "percent-encoding", "praxis-proxy-core", "praxis-proxy-filter", @@ -2103,7 +2459,7 @@ dependencies = [ [[package]] name = "praxis-ai-build-support" version = "0.2.0" -source = "git+https://github.com/praxis-proxy/ai?rev=45c075190c646c4e0397b29df9d944b7e49ed039#45c075190c646c4e0397b29df9d944b7e49ed039" +source = "git+https://github.com/praxis-proxy/ai?rev=5d6a4e822969ef4570d9e0cf4423087700b11136#5d6a4e822969ef4570d9e0cf4423087700b11136" dependencies = [ "cargo_metadata", ] @@ -2111,16 +2467,18 @@ dependencies = [ [[package]] name = "praxis-ai-filters" version = "0.2.0" -source = "git+https://github.com/praxis-proxy/ai?rev=45c075190c646c4e0397b29df9d944b7e49ed039#45c075190c646c4e0397b29df9d944b7e49ed039" +source = "git+https://github.com/praxis-proxy/ai?rev=5d6a4e822969ef4570d9e0cf4423087700b11136#5d6a4e822969ef4570d9e0cf4423087700b11136" dependencies = [ "arc-swap", "async-trait", + "aws-credential-types", + "aws-sigv4", "base64 0.23.1", "bytes", "chrono", "dashmap 6.2.1", "futures", - "http", + "http 1.5.0", "metrics", "notify", "praxis-ai-apis", @@ -2136,6 +2494,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "url", "yaml_serde", "zeroize", ] @@ -2143,7 +2502,7 @@ dependencies = [ [[package]] name = "praxis-ai-proxy" version = "0.2.0" -source = "git+https://github.com/praxis-proxy/ai?rev=45c075190c646c4e0397b29df9d944b7e49ed039#45c075190c646c4e0397b29df9d944b7e49ed039" +source = "git+https://github.com/praxis-proxy/ai?rev=5d6a4e822969ef4570d9e0cf4423087700b11136#5d6a4e822969ef4570d9e0cf4423087700b11136" dependencies = [ "cargo_metadata", "clap", @@ -2188,13 +2547,17 @@ dependencies = [ [[package]] name = "praxis-proxy-core" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55832a48bd2c8e8079b9e1023f14594ac4583b5b2803e3a0c334e5f73fe81c30" +source = "git+https://github.com/praxis-proxy/praxis.git?rev=1b439271#1b439271a2cf8eec2675b8c2a1ef76fc18bd0efc" dependencies = [ "bytes", + "chrono", "dashmap 6.2.1", - "http", + "http 1.5.0", "metrics", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "percent-encoding", "praxis-proxy-tls", "quixotic-plecostomus-core", "quixotic-plecostomus-http", @@ -2203,7 +2566,10 @@ dependencies = [ "serde", "thiserror 2.0.20", "tokio", + "tonic", "tracing", + "tracing-appender", + "tracing-opentelemetry", "tracing-subscriber", "yaml_serde", ] @@ -2211,14 +2577,14 @@ dependencies = [ [[package]] name = "praxis-proxy-filter" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be5f77647237cae66bd8c28a9dac8a7dbf72c6c8f9d769f1987fede0bd16dd4" +source = "git+https://github.com/praxis-proxy/praxis.git?rev=1b439271#1b439271a2cf8eec2675b8c2a1ef76fc18bd0efc" dependencies = [ "async-trait", "bytes", "dashmap 6.2.1", - "http", + "http 1.5.0", "metrics", + "opentelemetry", "percent-encoding", "praxis-proxy-core", "praxis-proxy-tls", @@ -2232,6 +2598,7 @@ dependencies = [ "thiserror 2.0.20", "tokio", "tracing", + "tracing-opentelemetry", "yaml_serde", "zeroize", ] @@ -2239,14 +2606,13 @@ dependencies = [ [[package]] name = "praxis-proxy-protocol" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7aebef930606ffa665975cc1101d1581ba86e2976e9f36e674c3ddd9d07d919" +source = "git+https://github.com/praxis-proxy/praxis.git?rev=1b439271#1b439271a2cf8eec2675b8c2a1ef76fc18bd0efc" dependencies = [ "arc-swap", "async-trait", "bytes", "futures", - "http", + "http 1.5.0", "metrics", "metrics-exporter-prometheus", "praxis-proxy-core", @@ -2255,6 +2621,7 @@ dependencies = [ "quixotic-plecostomus-core", "quixotic-plecostomus-http", "quixotic-plecostomus-proxy", + "rand 0.10.2", "serde", "serde_json", "tokio", @@ -2265,13 +2632,11 @@ dependencies = [ [[package]] name = "praxis-proxy-tls" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c7769f7d662c1cc13a868b24576d604cc30ea0fc0907153f16ca837402d91e" +source = "git+https://github.com/praxis-proxy/praxis.git?rev=1b439271#1b439271a2cf8eec2675b8c2a1ef76fc18bd0efc" dependencies = [ "arc-swap", "notify", "rustls", - "rustls-pemfile", "serde", "thiserror 2.0.20", "tokio", @@ -2320,6 +2685,38 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + [[package]] name = "quanta" version = "0.12.6" @@ -2410,7 +2807,7 @@ dependencies = [ "bstr", "bytes", "hex", - "http", + "http 1.5.0", "httparse", "httpdate", "indexmap 1.9.3", @@ -2453,7 +2850,7 @@ dependencies = [ "flurry", "futures", "h2", - "http", + "http 1.5.0", "httparse", "httpdate", "libc", @@ -2500,7 +2897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9fe7db0e7899a6b2d2cf06e71adacfad4c1bcfe3b5f238494dd3b3eafb2c1b1" dependencies = [ "bytes", - "http", + "http 1.5.0", "httparse", "quixotic-plecostomus-error", "quixotic-plecostomus-http", @@ -2516,7 +2913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f337131150ff271246df2584da8bdb0280b8424646d2c76e3926faa70409dc9" dependencies = [ "bytes", - "http", + "http 1.5.0", "quixotic-plecostomus-error", ] @@ -2559,7 +2956,7 @@ dependencies = [ "clap", "futures", "h2", - "http", + "http 1.5.0", "log", "once_cell", "quixotic-plecostomus-cache", @@ -2805,8 +3202,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-rustls", @@ -2859,7 +3256,7 @@ dependencies = [ "bytes", "chrono", "futures", - "http", + "http 1.5.0", "indexmap 2.14.0", "pin-project-lite", "reqwest", @@ -3550,7 +3947,7 @@ checksum = "c123f296ade4ec4b8b0f6162116e6629f5146922ca5ab40ca9d3c2e73ab4761e" dependencies = [ "bytes", "futures-util", - "http-body", + "http-body 1.1.0", "http-body-util", "pin-project-lite", ] @@ -3612,6 +4009,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -3899,6 +4302,57 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -3907,11 +4361,15 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap 2.14.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3923,8 +4381,8 @@ dependencies = [ "bitflags", "bytes", "futures-util", - "http", - "http-body", + "http 1.5.0", + "http-body 1.1.0", "pin-project-lite", "tower", "tower-layer", @@ -3956,6 +4414,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.20", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -3988,6 +4459,22 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" +dependencies = [ + "js-sys", + "opentelemetry", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" @@ -4147,6 +4634,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 6badfcc..60bb4aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,14 @@ members = [ "crates/praxis-experimental-server", ] +# Use praxis main for OTel features not yet in the v0.5.3 crates.io release +# (Tokio runtime fix for batch exporter, filter otel feature). +[patch.crates-io] +praxis-proxy-core = { git = "https://github.com/praxis-proxy/praxis.git", rev = "1b439271" } +praxis-proxy-filter = { git = "https://github.com/praxis-proxy/praxis.git", rev = "1b439271" } +praxis-proxy-tls = { git = "https://github.com/praxis-proxy/praxis.git", rev = "1b439271" } +praxis-proxy-protocol = { git = "https://github.com/praxis-proxy/praxis.git", rev = "1b439271" } + [workspace.package] version = "0.1.0" edition = "2024" diff --git a/Containerfile b/Containerfile index b459eff..7d237fa 100644 --- a/Containerfile +++ b/Containerfile @@ -12,6 +12,8 @@ RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconf cmake ma WORKDIR /src +ARG FEATURES="" + # ------------------------------------------------------------------------------ # Cache Build # ------------------------------------------------------------------------------ @@ -45,7 +47,7 @@ RUN mkdir -p crates/experimental-probe/src \ RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/src/target \ - cargo build --release -p praxis-experimental-server + cargo build --release -p praxis-experimental-server ${FEATURES:+--features "$FEATURES"} # ------------------------------------------------------------------------------ # Cache Tricks @@ -68,7 +70,7 @@ RUN find crates -name '*.rs' -exec touch {} + RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/src/target \ - cargo build --release -p praxis-experimental-server \ + cargo build --release -p praxis-experimental-server ${FEATURES:+--features "$FEATURES"} \ && cp target/release/praxis-experimental-server /usr/local/bin/praxis-experimental-server # ------------------------------------------------------------------------------ diff --git a/crates/praxis-experimental-server/Cargo.toml b/crates/praxis-experimental-server/Cargo.toml index 0ce9b0a..b527cdf 100644 --- a/crates/praxis-experimental-server/Cargo.toml +++ b/crates/praxis-experimental-server/Cargo.toml @@ -12,6 +12,13 @@ publish = false [lints] workspace = true +[features] +# praxis-ai-proxy's own feature is named "opentelemetry" (not "otel"); it +# gates AI-specific spans such as intelligent_route's routing.select +# (praxis-ai-filters/src/opentelemetry.rs). Without it, core request/filter +# spans still export, but AI routing-decision spans are compiled out. +otel = ["praxis-core/otel", "praxis-filter/otel", "praxis-ai-proxy/opentelemetry"] + # cargo-machete's source scan can't see these usages: # - praxis-experimental-filters is consumed by the discovery build script (its presence # as a direct dep, carrying the praxis-filters marker, is the point). @@ -42,10 +49,11 @@ praxis-core = { version = "0.5.3", package = "praxis-proxy-core" } # `http-callout-filter` is off by default upstream because its config surface is # still moving. This image exists to expose experimental work, so it is on here # — it is what backs the Lakera Guard example config. -praxis-ai-proxy = { git = "https://github.com/praxis-proxy/ai", rev = "45c075190c646c4e0397b29df9d944b7e49ed039", features = [ +praxis-ai-proxy = { git = "https://github.com/praxis-proxy/ai", rev = "5d6a4e822969ef4570d9e0cf4423087700b11136", features = [ "http-callout-filter", + "praxis-main", ] } [build-dependencies] cargo_metadata = "0.23.1" -praxis-ai-build-support = { git = "https://github.com/praxis-proxy/ai", rev = "45c075190c646c4e0397b29df9d944b7e49ed039" } +praxis-ai-build-support = { git = "https://github.com/praxis-proxy/ai", rev = "5d6a4e822969ef4570d9e0cf4423087700b11136" } diff --git a/demos/README.md b/demos/README.md index da1cf92..348a9cb 100644 --- a/demos/README.md +++ b/demos/README.md @@ -6,3 +6,4 @@ Demonstrations of experimental Praxis features live in this directory. - [Praxis Grid - Distributed token rate limiting with Grid routing](grid-distributed-token-rate-limit/README.md) - [Praxis Grid - Intelligent Overflow](grid-cloud-burst/README.md) +- [OTel observability benchmark](otel-benchmark/README.md) diff --git a/demos/otel-benchmark/README-core.md b/demos/otel-benchmark/README-core.md new file mode 100644 index 0000000..c931e17 --- /dev/null +++ b/demos/otel-benchmark/README-core.md @@ -0,0 +1,51 @@ +# Core Proxy OTel Benchmark + +Benchmarks OTel tracing overhead on the core Praxis proxy using +`GET /` against a Fortio echo backend. Shares the same cluster and +observability stack as the [AI benchmark](README.md). + +## Prerequisites + +Complete steps 1-6 from the [AI benchmark README](README.md) first. +The cluster, stacks, and images must already be deployed. + +## Send core traffic + +```bash +for i in $(seq 1 100); do + curl -sf http://localhost:18080/ +done +``` + +10 spans per GET request: + +```text +GET / -> echo-backend (root) + |-- filter:request_id:request + |-- filter:access_log:request + |-- filter:router:request -> routes / to echo cluster + |-- filter:load_balancer:request + |-- filter:load_balancer:response + |-- filter:router:response + |-- filter:access_log:response + |-- filter:request_id:response + +-- upstream_exchange [echo-backend:8080] +``` + +## Run the benchmark + +```bash +bash scripts/benchmark.sh --scenario core +``` + +Runs 3 configurations at 2000 RPS for 30s each: +- **A: Baseline** — `praxis-experimental:dev` (no OTel feature) +- **B: OTel noop** — `praxis-experimental:dev-otel` (spans created, + not exported) +- **C: OTel full** — `praxis-experimental:dev-otel` (spans exported + to collector -> Tempo) + +Generate the report: +```bash +bash scripts/report.sh +``` diff --git a/demos/otel-benchmark/README.md b/demos/otel-benchmark/README.md new file mode 100644 index 0000000..98e90e4 --- /dev/null +++ b/demos/otel-benchmark/README.md @@ -0,0 +1,196 @@ +# AI Gateway OTel Benchmark + +Benchmarks OTel tracing overhead on the Praxis experimental AI gateway +using `POST /v1/chat/completions` against a mock LLM backend (llm-d +inference-sim). Deploys a full observability stack on KIND. + +For the core (non-AI) proxy benchmark, see [README-core.md](README-core.md). + +## Stack + +| Component | Purpose | +|-----------|---------| +| Praxis experimental AI gateway | Proxy under test (baseline + OTel) | +| llm-d inference-sim | Mock LLM backend | +| Fortio echo | Mock HTTP backend (core scenarios) | +| Prometheus + Grafana 11.x | Metrics + dashboards | +| Tempo | Distributed trace storage | +| Loki + Promtail | Log aggregation | +| OTel Collector | Trace pipeline (OTLP -> Tempo) | +| MLflow | Experiment tracking UI | + +## Prerequisites + +1. Docker or Podman +2. [KIND](https://kind.sigs.k8s.io/) +3. [Helm](https://helm.sh/) with repos added (step 1 below) +4. [vegeta](https://github.com/tsenart/vegeta) (for benchmarks) +5. [Praxis Forge CLI](https://github.com/praxis-proxy/forge) with + `extraPortMappings` support (praxis-proxy/forge#14): + ```bash + cargo install --locked --git https://github.com/praxis-proxy/forge --branch feat/extra-port-mappings + ``` + Verify: `praxis-forge doctor` + +## Step-by-Step + +### 1. Add Helm repos (one-time) + +```bash +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo add grafana https://grafana.github.io/helm-charts +helm repo add community-charts https://community-charts.github.io/helm-charts +helm repo update +``` + +### 2. Build images + +From the **experimental repo root**: + +```bash +cd /path/to/experimental + +# Baseline (no OTel) +docker build -t praxis-experimental:dev -f Containerfile . + +# With OTel tracing +docker build --build-arg FEATURES=otel -t praxis-experimental:dev-otel -f Containerfile . +``` + +### 3. Create the KIND cluster + +```bash +cd demos/otel-benchmark +praxis-forge up --config forge.yaml +``` + +### 4. Load images into KIND + +```bash +kind load docker-image praxis-experimental:dev praxis-experimental:dev-otel --name otel-bench-local +``` + +### 5. Deploy all stacks + +```bash +for stack in prometheus tempo loki otel-collector mlflow mock-backends praxis-deploy dashboards datasources; do + praxis-forge apply --config forge.yaml local "$stack" +done +``` + +### 6. Verify + +```bash +praxis-forge status --config forge.yaml +curl http://localhost:18080/ # proxy responds +open http://localhost:13000 # Grafana (admin/admin) +open http://localhost:19090 # Prometheus +open http://localhost:15000 # MLflow +``` + +### 7. Send AI traffic + +```bash +for i in $(seq 1 100); do + curl -sf http://localhost:18080/v1/chat/completions \ + -X POST -H 'Content-Type: application/json' \ + -d '{"model":"test-model","messages":[{"role":"user","content":"hello"}]}' +done +``` + +### 8. View traces + +Open Grafana > **Praxis OTel Traces** dashboard. Click a trace ID to see +the span waterfall: + +```text +POST /v1/chat/completions -> inference-sim (root) + |-- filter:request_id:request + |-- filter:access_log:request + |-- filter:model_to_header:request + |-- filter:model_to_header:request_body -> promotes body "model" to X-Model header + |-- filter:token_usage_headers:request + |-- filter:token_count:request + |-- filter:time_to_first_token:request + |-- filter:intelligent_route:request + | +-- routing.select -> candidate scoring (inference wins, fresh) + |-- filter:load_balancer:request + |-- filter:load_balancer:response + |-- filter:intelligent_route:response + |-- filter:time_to_first_token:response + |-- filter:token_count:response + |-- filter:token_count:response_body -> extracts usage.prompt/completion_tokens + |-- filter:token_usage_headers:response -> injects Praxis-Token-* headers + |-- filter:model_to_header:response + |-- filter:access_log:response + |-- filter:request_id:response + |-- filter:access_log:response_body + +-- upstream_exchange [inference-sim:8000] +``` + +Exact span count and order are captured live from Tempo; see the benchmark +report for the authoritative per-request span list. The `routing.select` +span is a child of `filter:intelligent_route:request`. Note: `intelligent_route` +is the sole cluster-selecting filter in the AI configs — Praxis's pipeline +validator rejects a chain with both `router` and `intelligent_route` ahead of +`load_balancer` (both implement `selects_cluster() -> true`), so the `ai` +benchmark scenario (which only sends `POST /v1/chat/completions`) relies on +`intelligent_route` alone to select the `inference` cluster. + +### 9. Run the benchmark + +```bash +bash scripts/benchmark.sh --scenario ai +``` + +Runs 3 configurations at 500 RPS for 30s each: +- **A: Baseline** — `praxis-experimental:dev` (no OTel feature) +- **B: OTel noop** — `praxis-experimental:dev-otel` (spans created, + not exported) +- **C: OTel full** — `praxis-experimental:dev-otel` (spans exported + to collector -> Tempo) + +Generate the report: +```bash +bash scripts/report.sh +``` + +### 10. Teardown + +```bash +praxis-forge down --config forge.yaml +``` + +## Dashboards + +| Dashboard | URL | +| --- | --- | +| Praxis Proxy Overview | | +| Praxis OTel Traces | | +| Praxis Benchmark Results | | +| Praxis AI/LLM Golden Signals | | +| Praxis Structured Logs | | + +## Host Ports + +| Port | Service | KIND NodePort | +| --- | --- | --- | +| 18080 | Praxis proxy | 30080 | +| 18901 | Praxis admin | 30901 | +| 13000 | Grafana | 30300 | +| 19090 | Prometheus | 30909 | +| 15000 | MLflow | 30500 | + +## Known Issues + +- **Grafana 11.x required**: Pinned via `grafana.image.tag`. Grafana 12.0 has + rendering bugs with provisioned dashboards. +- **Datasources**: Added via the `datasources` stack. If Grafana restarts, + re-run the datasources stack. +- **MLflow sqlite**: Uses file-backed sqlite on an emptyDir volume. + Data survives container restarts but not pod eviction. +- **AI token metrics**: The Tokens/sec and Hourly Cost panels require upstream + work (ai#141). +- **praxis crate patch**: `Cargo.toml` patches praxis crates to main + (`1b439271`) for the Tokio runtime fix and filter/otel feature not yet in + v0.5.3. Remove the `[patch.crates-io]` section once praxis publishes v0.5.4+. diff --git a/demos/otel-benchmark/configs/ai-baseline.yaml b/demos/otel-benchmark/configs/ai-baseline.yaml new file mode 100644 index 0000000..d63abee --- /dev/null +++ b/demos/otel-benchmark/configs/ai-baseline.yaml @@ -0,0 +1,58 @@ +# AI baseline benchmark config (no OTel). +# Same extended filter chain as ai-otel-full.yaml, without telemetry, so +# the OTel overhead comparison is apples-to-apples. +# intelligent_route is the sole cluster-selecting filter (see +# ai-otel-full.yaml for why `router` is not used alongside it); the `ai` +# benchmark scenario only sends POST /v1/chat/completions traffic, which +# intelligent_route routes to inference-sim. + +admin: + address: "0.0.0.0:9901" + +insecure_options: + allow_public_admin: true + +listeners: + - name: default + address: "0.0.0.0:8080" + filter_chains: [main] + +filter_chains: + - name: main + filters: + - filter: request_id + - filter: access_log + + - filter: model_to_header + + - filter: token_usage_headers + + - filter: token_count + provider: openai + + - filter: time_to_first_token + + - filter: intelligent_route + local_site: site-a + model_header: X-Model + candidates: + - kind: inference_model + name: test-model + site: site-a + cluster: inference + fresh: true + + - kind: inference_model + name: test-model + site: site-a + cluster: echo + fresh: false + + - filter: load_balancer + clusters: + - name: echo + endpoints: + - "echo-backend.default.svc:8080" + - name: inference + endpoints: + - "inference-sim.default.svc:8000" diff --git a/demos/otel-benchmark/configs/ai-otel-full.yaml b/demos/otel-benchmark/configs/ai-otel-full.yaml new file mode 100644 index 0000000..68cfaed --- /dev/null +++ b/demos/otel-benchmark/configs/ai-otel-full.yaml @@ -0,0 +1,94 @@ +# AI OTel full. +# Extended AI filter chain: request_id, access_log, model_to_header, +# token_usage_headers, token_count, time_to_first_token, intelligent_route, +# load_balancer. Spans exported to collector. +# +# Filter order notes: +# - token_usage_headers is declared BEFORE token_count: response hooks +# run in reverse declared order, so token_count's on_response_body +# (which populates filter_metadata token.input/output/total) runs +# before token_usage_headers reads it and injects the +# Praxis-Token-* response headers. +# - intelligent_route is the sole cluster-selecting filter here. +# Praxis's pipeline validator rejects a chain with more than one +# cluster-selecting filter ahead of load_balancer (router and +# intelligent_route both implement `selects_cluster() -> true`), so +# the plain path-prefix `router` filter used by the non-AI baseline +# configs is dropped in favor of intelligent_route, which resolves +# the model name (promoted to X-Model by model_to_header) against +# its static candidates and records the routing.select span. The +# inference candidate is fresh (high score) and always wins over the +# stale echo fallback. The `ai` benchmark scenario only sends +# POST /v1/chat/completions traffic, so this is sufficient; the echo +# cluster stays defined in load_balancer to demonstrate the +# fallback-candidate shape but is not reachable by this config's +# benchmark traffic. + +admin: + address: "0.0.0.0:9901" + +insecure_options: + allow_public_admin: true + +telemetry: + otlp_endpoint: "http://otel-collector.otel.svc:4317" + +listeners: + - name: default + address: "0.0.0.0:8080" + filter_chains: [main] + +filter_chains: + - name: main + filters: + - filter: request_id + - filter: access_log + + # Extract the model field from the JSON request body onto the + # default X-Model header for downstream routing. + - filter: model_to_header + + # token_usage_headers must precede token_count in the filter list + # (response hooks run in reverse order). Note: for the openai + # provider (and every provider currently supported), token_count + # extracts usage from the response body inside on_response_body, + # which runs after response headers are already sent downstream, + # so the Praxis-Token-* headers are never actually injected on the + # wire today (see ai/examples/configs/token-counting.yaml). The + # counts still land in filter_metadata for other response-phase + # filters (and traces/metrics) to read. + - filter: token_usage_headers + + - filter: token_count + provider: openai + + - filter: time_to_first_token + + # Score-based routing over static candidates keyed on the X-Model + # header. inference-sim serves "test-model" and is fresh (high + # score); echo-backend is a stale fallback (low score) that never + # wins while inference-sim is admitted. + - filter: intelligent_route + local_site: site-a + model_header: X-Model + candidates: + - kind: inference_model + name: test-model + site: site-a + cluster: inference + fresh: true + + - kind: inference_model + name: test-model + site: site-a + cluster: echo + fresh: false + + - filter: load_balancer + clusters: + - name: echo + endpoints: + - "echo-backend.default.svc:8080" + - name: inference + endpoints: + - "inference-sim.default.svc:8000" diff --git a/demos/otel-benchmark/configs/baseline.yaml b/demos/otel-benchmark/configs/baseline.yaml new file mode 100644 index 0000000..3658fd2 --- /dev/null +++ b/demos/otel-benchmark/configs/baseline.yaml @@ -0,0 +1,31 @@ +# Core baseline (no OTel). +# Same filter chain as otel-full for apples-to-apples comparison. + +admin: + address: "0.0.0.0:9901" + +insecure_options: + allow_public_admin: true + +listeners: + - name: default + address: "0.0.0.0:8080" + filter_chains: [main] + +filter_chains: + - name: main + filters: + - filter: request_id + + - filter: access_log + + - filter: router + routes: + - path_prefix: "/" + cluster: echo + + - filter: load_balancer + clusters: + - name: echo + endpoints: + - "echo-backend.default.svc:8080" diff --git a/demos/otel-benchmark/configs/otel-full.yaml b/demos/otel-benchmark/configs/otel-full.yaml new file mode 100644 index 0000000..e7e62df --- /dev/null +++ b/demos/otel-benchmark/configs/otel-full.yaml @@ -0,0 +1,34 @@ +# Core OTel full. +# Spans exported via OTLP/gRPC to the collector. + +admin: + address: "0.0.0.0:9901" + +insecure_options: + allow_public_admin: true + +telemetry: + otlp_endpoint: "http://otel-collector.otel.svc:4317" + +listeners: + - name: default + address: "0.0.0.0:8080" + filter_chains: [main] + +filter_chains: + - name: main + filters: + - filter: request_id + + - filter: access_log + + - filter: router + routes: + - path_prefix: "/" + cluster: echo + + - filter: load_balancer + clusters: + - name: echo + endpoints: + - "echo-backend.default.svc:8080" diff --git a/demos/otel-benchmark/forge.yaml b/demos/otel-benchmark/forge.yaml new file mode 100644 index 0000000..90edfc3 --- /dev/null +++ b/demos/otel-benchmark/forge.yaml @@ -0,0 +1,176 @@ +apiVersion: forge.praxis.dev/v1alpha1 +kind: Environment +metadata: + name: otel-benchmark +spec: + runtime: + provider: docker + clusterPrefix: otel-bench + network: + crossCluster: false + + clusters: + - name: local + ports: + - host: 18080 + container: 30080 # Praxis proxy + - host: 18901 + container: 30901 # Praxis admin/metrics + - host: 13000 + container: 30300 # Grafana + - host: 19090 + container: 30909 # Prometheus + - host: 15000 + container: 30500 # MLflow + stacks: + - prometheus + - tempo + - loki + - otel-collector + - mlflow + - mock-backends + - praxis-deploy + - dashboards + - datasources + properties: + # Pin chart versions for reproducibility + kubePrometheusVersion: "72.6.3" + tempoVersion: "1.24.4" + lokiStackVersion: "2.10.2" + mlflowVersion: "1.11.3" + + stacks: + prometheus: + description: kube-prometheus-stack (Prometheus + Grafana) + steps: + - type: helm + release: monitoring + chart: prometheus-community/kube-prometheus-stack + version: "{{ cluster.properties.kubePrometheusVersion }}" + namespace: monitoring + values: + prometheus: + prometheusSpec: + serviceMonitorSelectorNilUsesHelmValues: false + additionalScrapeConfigs: + - job_name: praxis-proxy + metrics_path: /metrics + scrape_interval: 15s + static_configs: + - targets: ["praxis-proxy.default.svc:9901"] + service: + type: NodePort + nodePort: 30909 + grafana: + adminPassword: admin + image: + tag: 11.6.0 + service: + type: NodePort + nodePort: 30300 + - type: wait + resource: deployment/monitoring-grafana + namespace: monitoring + condition: available + timeout: "300s" + + tempo: + description: Tempo trace storage + steps: + - type: helm + release: tempo + chart: grafana/tempo + version: "{{ cluster.properties.tempoVersion }}" + namespace: monitoring + + loki: + description: Loki + Promtail log aggregation + steps: + - type: helm + release: loki + chart: grafana/loki-stack + version: "{{ cluster.properties.lokiStackVersion }}" + namespace: monitoring + values: + loki: + persistence: + enabled: false + promtail: + enabled: true + + otel-collector: + description: OpenTelemetry Collector (OTLP → Tempo) + steps: + - type: exec + command: [bash, -c, "kubectl create namespace otel --dry-run=client -o yaml | kubectl apply -f -"] + - type: manifest + path: stacks/observability/manifests/otel-collector.yaml + - type: wait + resource: deployment/otel-collector + namespace: otel + condition: available + timeout: "60s" + + mlflow: + description: MLflow experiment tracking + steps: + - type: helm + release: mlflow + chart: community-charts/mlflow + version: "{{ cluster.properties.mlflowVersion }}" + namespace: mlflow + values: + backendStore: + databaseMigration: true + defaultSqlitePath: /mlflow-data/mlflow.db + extraVolumes: + - name: mlflow-data + emptyDir: {} + extraVolumeMounts: + - name: mlflow-data + mountPath: /mlflow-data + service: + type: NodePort + - type: exec + command: [bash, -c, "kubectl -n mlflow patch svc mlflow --type=json -p '[{\"op\":\"replace\",\"path\":\"/spec/ports/0/nodePort\",\"value\":30500}]'"] + + mock-backends: + description: Fortio echo + llm-d inference-sim + steps: + - type: manifest + path: stacks/mock-backends/manifests/echo-backend.yaml + - type: manifest + path: stacks/mock-backends/manifests/inference-sim.yaml + + praxis-deploy: + description: Deploy Praxis proxy with OTel config + steps: + - type: exec + command: [bash, -c, "kubectl create configmap praxis-config --from-file=praxis.yaml=configs/ai-otel-full.yaml -n default --dry-run=client -o yaml | kubectl apply -f -"] + - type: manifest + path: manifests/praxis.yaml + - type: manifest + path: manifests/servicemonitor.yaml + - type: wait + resource: deployment/praxis-proxy + namespace: default + condition: available + timeout: "120s" + + dashboards: + description: Load Grafana dashboards via ConfigMap + steps: + - type: exec + command: [bash, -c, "kubectl -n monitoring create configmap praxis-dashboards --from-file=stacks/observability/dashboards/ --dry-run=client -o yaml | kubectl apply -f -"] + - type: exec + command: [kubectl, -n, monitoring, label, configmap, praxis-dashboards, grafana_dashboard=1, --overwrite] + + datasources: + description: Add Prometheus + Tempo + Loki datasources to Grafana + steps: + - type: exec + command: [bash, -c, "for i in 1 2 3 4 5; do curl -sf -u admin:admin -X POST http://localhost:13000/api/datasources -H 'Content-Type: application/json' -d '{\"name\":\"Prometheus\",\"type\":\"prometheus\",\"access\":\"proxy\",\"url\":\"http://monitoring-kube-prometheus-prometheus.monitoring.svc:9090\",\"uid\":\"prometheus\"}' && break || sleep $((i*3)); done"] + - type: exec + command: [bash, -c, "for i in 1 2 3 4 5; do curl -sf -u admin:admin -X POST http://localhost:13000/api/datasources -H 'Content-Type: application/json' -d '{\"name\":\"Tempo\",\"type\":\"tempo\",\"access\":\"proxy\",\"url\":\"http://tempo.monitoring.svc:3200\",\"uid\":\"tempo\"}' && break || sleep $((i*3)); done"] + - type: exec + command: [bash, -c, "for i in 1 2 3 4 5; do curl -sf -u admin:admin -X POST http://localhost:13000/api/datasources -H 'Content-Type: application/json' -d '{\"name\":\"Loki\",\"type\":\"loki\",\"access\":\"proxy\",\"url\":\"http://loki.monitoring.svc:3100\",\"uid\":\"loki\"}' && break || sleep $((i*3)); done"] diff --git a/demos/otel-benchmark/manifests/praxis.yaml b/demos/otel-benchmark/manifests/praxis.yaml new file mode 100644 index 0000000..58bbd94 --- /dev/null +++ b/demos/otel-benchmark/manifests/praxis.yaml @@ -0,0 +1,73 @@ +# Praxis Deployment for benchmark mode. +# ConfigMap is created by the praxis-deploy stack (forge.yaml) +# or by benchmark.sh/benchmark-ai.sh when swapping configs between runs. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: praxis-proxy + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: praxis-proxy + template: + metadata: + labels: + app: praxis-proxy + spec: + containers: + - name: praxis-proxy + image: praxis-experimental:dev-otel + ports: + - containerPort: 8080 + name: proxy + - containerPort: 9901 + name: admin + volumeMounts: + - name: config + mountPath: /etc/praxis + resources: + requests: + cpu: 500m + memory: 128Mi + limits: + cpu: "2" + memory: 512Mi + readinessProbe: + httpGet: + path: /healthy + port: admin + initialDelaySeconds: 2 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /healthy + port: admin + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: praxis-config +--- +apiVersion: v1 +kind: Service +metadata: + name: praxis-proxy + namespace: default + labels: + app: praxis-proxy +spec: + type: NodePort + selector: + app: praxis-proxy + ports: + - port: 8080 + targetPort: 8080 + nodePort: 30080 + name: proxy + - port: 9901 + targetPort: 9901 + nodePort: 30901 + name: admin diff --git a/demos/otel-benchmark/manifests/servicemonitor.yaml b/demos/otel-benchmark/manifests/servicemonitor.yaml new file mode 100644 index 0000000..312a057 --- /dev/null +++ b/demos/otel-benchmark/manifests/servicemonitor.yaml @@ -0,0 +1,15 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: praxis-proxy + namespace: default + labels: + app: praxis-proxy +spec: + selector: + matchLabels: + app: praxis-proxy + endpoints: + - port: admin + interval: 15s + path: /metrics diff --git a/demos/otel-benchmark/scripts/ai-payload.json b/demos/otel-benchmark/scripts/ai-payload.json new file mode 100644 index 0000000..9f03b09 --- /dev/null +++ b/demos/otel-benchmark/scripts/ai-payload.json @@ -0,0 +1 @@ +{"model":"test-model","messages":[{"role":"user","content":"hello"}]} diff --git a/demos/otel-benchmark/scripts/benchmark.sh b/demos/otel-benchmark/scripts/benchmark.sh new file mode 100755 index 0000000..2ecf987 --- /dev/null +++ b/demos/otel-benchmark/scripts/benchmark.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +for cmd in vegeta kubectl kind; do + command -v "$cmd" >/dev/null 2>&1 || { echo "Error: $cmd not found"; exit 1; } +done + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DEMO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +CLUSTER_NAME="${CLUSTER_NAME:-otel-bench-local}" +CTX="kind-${CLUSTER_NAME}" +GATEWAY_URL="http://localhost:18080" + +DURATION="${DURATION:-30s}" +RUNS="${RUNS:-3}" + +# ---- Argument parsing ---- +SCENARIO="ai" +while [[ $# -gt 0 ]]; do + case "$1" in + --scenario) + SCENARIO="${2:?missing value for --scenario}" + shift 2 + ;; + --scenario=*) + SCENARIO="${1#*=}" + shift + ;; + -h|--help) + echo "Usage: $0 [--scenario core|ai]" + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + echo "Usage: $0 [--scenario core|ai]" >&2 + exit 1 + ;; + esac +done + +# ---- vegeta target generators ---- +core_target() { + echo "GET ${GATEWAY_URL}/" +} + +ai_target() { + printf 'POST %s/v1/chat/completions\nContent-Type: application/json\n@%s\n' \ + "${GATEWAY_URL}" "${SCRIPT_DIR}/ai-payload.json" +} + +# ---- Scenario parameters ---- +case "${SCENARIO}" in + core) + RATE="${RATE:-2000}" + CONNECTIONS=200 + WARMUP_RATE=500 + CONFIGS=(baseline baseline otel-full) + LABELS=(baseline otel-noop otel-full) + RESULTS_PREFIX="" + TARGET_FN=core_target + SCENARIO_TITLE="" + REPORT_SCRIPT="report.sh" + ;; + ai) + RATE="${RATE:-500}" + CONNECTIONS=100 + WARMUP_RATE=100 + CONFIGS=(ai-baseline ai-baseline ai-otel-full) + LABELS=(ai-baseline ai-otel-noop ai-otel-full) + RESULTS_PREFIX="ai-" + TARGET_FN=ai_target + SCENARIO_TITLE="AI " + REPORT_SCRIPT="report.sh" + ;; + *) + echo "Usage: $0 [--scenario core|ai]" >&2 + exit 1 + ;; +esac + +RESULTS_DIR="${DEMO_DIR}/results/${RESULTS_PREFIX}$(date +%Y%m%d-%H%M%S)" +mkdir -p "${RESULTS_DIR}" + +echo "=== Praxis ${SCENARIO_TITLE}OTel Benchmark ===" +echo "Rate: ${RATE} RPS | Duration: ${DURATION} | Runs: ${RUNS}" +echo "Results: ${RESULTS_DIR}" +echo "" + +run_vegeta() { + local label="$1" + local run="$2" + echo "--- ${label} run ${run}/${RUNS} ---" + "${TARGET_FN}" | \ + vegeta attack -rate="${RATE}" -duration="${DURATION}" -connections="${CONNECTIONS}" | \ + tee "${RESULTS_DIR}/${label}-run${run}.bin" | \ + vegeta report -type=json > "${RESULTS_DIR}/${label}-run${run}.json" + vegeta report < "${RESULTS_DIR}/${label}-run${run}.bin" + # Capture resource snapshot + kubectl --context "${CTX}" top pod -n default --no-headers 2>/dev/null \ + >> "${RESULTS_DIR}/${label}-resources.txt" || true + echo "" +} + +# ---- Run A/B/C definitions ---- +RUN_LETTERS=(A B C) +RUN_DESCRIPTIONS=( + "${SCENARIO_TITLE}Baseline (praxis-experimental:dev, no OTel)" + "${SCENARIO_TITLE}OTel noop (praxis-experimental:dev-otel, no endpoint)" + "${SCENARIO_TITLE}OTel full (praxis-experimental:dev-otel, exporting)" +) +IMAGES=(dev dev-otel dev-otel) + +for idx in 0 1 2; do + echo "==========================================" + echo " Run ${RUN_LETTERS[$idx]}: ${RUN_DESCRIPTIONS[$idx]}" + echo "==========================================" + + kubectl --context "${CTX}" create configmap praxis-config \ + --from-file=praxis.yaml="${DEMO_DIR}/configs/${CONFIGS[$idx]}.yaml" \ + -n default --dry-run=client -o yaml | kubectl --context "${CTX}" apply -f - + kubectl --context "${CTX}" set image deployment/praxis-proxy praxis-proxy=praxis-experimental:"${IMAGES[$idx]}" -n default + + if [[ "${idx}" -eq 2 ]]; then + # Set OTEL env var BEFORE restarting the pod + kubectl --context "${CTX}" set env deployment/praxis-proxy \ + OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.otel.svc:4317 -n default + else + kubectl --context "${CTX}" set env deployment/praxis-proxy OTEL_EXPORTER_OTLP_ENDPOINT- -n default 2>/dev/null || true + fi + + kubectl --context "${CTX}" scale deployment/praxis-proxy --replicas=0 -n default + sleep 3 + kubectl --context "${CTX}" scale deployment/praxis-proxy --replicas=1 -n default + kubectl --context "${CTX}" -n default wait --for=condition=Available deployment/praxis-proxy --timeout 60s + sleep 5 + + echo "Warmup..." + "${TARGET_FN}" | vegeta attack -rate="${WARMUP_RATE}" -duration=10s > /dev/null 2>&1 || true + sleep 2 + + for i in $(seq 1 "${RUNS}"); do + run_vegeta "${LABELS[$idx]}" "${i}" + sleep 5 + done +done + +echo "==========================================" +echo " ${SCENARIO_TITLE}Benchmark complete" +echo "==========================================" +echo "Results in: ${RESULTS_DIR}" +echo "" +echo "Generate report:" +echo " bash ${SCRIPT_DIR}/${REPORT_SCRIPT} ${RESULTS_DIR}" diff --git a/demos/otel-benchmark/scripts/report.sh b/demos/otel-benchmark/scripts/report.sh new file mode 100755 index 0000000..6396b53 --- /dev/null +++ b/demos/otel-benchmark/scripts/report.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +RESULTS_DIR="${1:?Usage: report.sh }" + +if [ ! -d "${RESULTS_DIR}" ]; then + echo "Error: ${RESULTS_DIR} does not exist" + exit 1 +fi + +export RESULTS_DIR +export BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +export COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + +# Auto-detect scenario from results directory contents +if [ -f "${RESULTS_DIR}/ai-baseline-run1.json" ]; then + export CONFIGS="ai-baseline ai-otel-noop ai-otel-full" + export BASELINE_LABEL="AI Baseline" + export NOOP_LABEL="AI OTel (noop)" + export FULL_LABEL="AI OTel (full)" + export REPORT_TITLE="Praxis AI OTel Overhead Benchmark" + export BACKEND_DESC="inference-sim (mock LLM)" +else + export CONFIGS="baseline otel-noop otel-full" + export BASELINE_LABEL="Baseline" + export NOOP_LABEL="OTel (noop)" + export FULL_LABEL="OTel (full)" + export REPORT_TITLE="Praxis OTel Overhead Benchmark" + export BACKEND_DESC="Fortio echo" +fi + +REPORT="${RESULTS_DIR}/report.md" + +# Use Python to generate the report (macOS has bash 3.2, no associative arrays) +python3 << 'PYTHON_SCRIPT' +import json +import glob +import sys +import os +from datetime import datetime + +results_dir = os.environ['RESULTS_DIR'] +branch = os.environ['BRANCH'] +commit = os.environ['COMMIT'] +configs = os.environ['CONFIGS'].split() +baseline_label = os.environ['BASELINE_LABEL'] +noop_label = os.environ['NOOP_LABEL'] +full_label = os.environ['FULL_LABEL'] +report_title = os.environ['REPORT_TITLE'] +backend_desc = os.environ['BACKEND_DESC'] + +baseline_config, noop_config, full_config = configs +stats = {} + +for config in configs: + total_p50 = 0 + total_p99 = 0 + total_rps = 0 + count = 0 + + pattern = os.path.join(results_dir, f'{config}-run*.json') + for path in glob.glob(pattern): + try: + with open(path) as f: + data = json.load(f) + total_p50 += data['latencies']['50th'] + total_p99 += data['latencies']['99th'] + total_rps += data['throughput'] + count += 1 + except (json.JSONDecodeError, KeyError) as e: + print(f"Warning: Failed to parse {path}: {e}", file=sys.stderr) + continue + + if count > 0: + stats[config] = { + 'p50': int(total_p50 / count / 1000), # Convert to microseconds + 'p99': int(total_p99 / count / 1000), + 'rps': int(total_rps / count), + 'runs': count + } + else: + stats[config] = {'p50': 0, 'p99': 0, 'rps': 0, 'runs': 0} + +# Generate report +report_path = os.path.join(results_dir, 'report.md') +with open(report_path, 'w') as f: + f.write(f"# {report_title}\n\n") + f.write(f"**Date:** {datetime.now().strftime('%Y-%m-%d')} | **Commit:** {commit} | **Branch:** {branch}\n") + f.write(f"**Platform:** KIND (1 node) | **Backend:** {backend_desc}\n\n") + f.write(f"## Summary\n\n") + f.write(f"| Config | P50 (us) | P99 (us) | RPS | Runs | P50 delta | P99 delta |\n") + f.write(f"|--------|----------|----------|-----|------|-----------|-----------|\n") + + base_p50 = stats[baseline_config]['p50'] + base_p99 = stats[baseline_config]['p99'] + + for config in configs: + s = stats[config] + p50 = s['p50'] + p99 = s['p99'] + rps = s['rps'] + runs = s['runs'] + + if config == baseline_config: + f.write(f"| {baseline_label} | {p50} | {p99} | {rps} | {runs} | -- | -- |\n") + else: + label = noop_label if config == noop_config else full_label + + if base_p50 > 0: + dp50 = f"{(p50 - base_p50) / base_p50 * 100:+.1f}%" + dp99 = f"{(p99 - base_p99) / base_p99 * 100:+.1f}%" + else: + dp50 = "N/A" + dp99 = "N/A" + + f.write(f"| {label} | {p50} | {p99} | {rps} | {runs} | {dp50} | {dp99} |\n") + +print(f"Report written to: {report_path}") +PYTHON_SCRIPT + +echo "" +cat "${REPORT}" diff --git a/demos/otel-benchmark/stacks/mock-backends/manifests/echo-backend.yaml b/demos/otel-benchmark/stacks/mock-backends/manifests/echo-backend.yaml new file mode 100644 index 0000000..5eeae10 --- /dev/null +++ b/demos/otel-benchmark/stacks/mock-backends/manifests/echo-backend.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: echo-backend + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: echo-backend + template: + metadata: + labels: + app: echo-backend + spec: + containers: + - name: fortio + image: fortio/fortio:1.75.2 + args: ["server", "-echo-server-default-params", "delay=0"] + ports: + - containerPort: 8080 + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: "1" + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: echo-backend + namespace: default +spec: + selector: + app: echo-backend + ports: + - port: 8080 + targetPort: 8080 diff --git a/demos/otel-benchmark/stacks/mock-backends/manifests/inference-sim.yaml b/demos/otel-benchmark/stacks/mock-backends/manifests/inference-sim.yaml new file mode 100644 index 0000000..16badc7 --- /dev/null +++ b/demos/otel-benchmark/stacks/mock-backends/manifests/inference-sim.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: inference-sim + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: inference-sim + template: + metadata: + labels: + app: inference-sim + spec: + containers: + - name: sim + image: ghcr.io/llm-d/llm-d-inference-sim:v0.10.2 + args: ["--port", "8000", "--model", "test-model", "--mode", "random"] + ports: + - containerPort: 8000 + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: "1" + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: inference-sim + namespace: default +spec: + selector: + app: inference-sim + ports: + - port: 8000 + targetPort: 8000 diff --git a/demos/otel-benchmark/stacks/observability/dashboards/praxis-ai-golden-signals.json b/demos/otel-benchmark/stacks/observability/dashboards/praxis-ai-golden-signals.json new file mode 100644 index 0000000..295b635 --- /dev/null +++ b/demos/otel-benchmark/stacks/observability/dashboards/praxis-ai-golden-signals.json @@ -0,0 +1,173 @@ +{ + "editable": true, + "graphTooltip": 1, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.5 + }, + { + "color": "red", + "value": 2 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ] + } + }, + "targets": [ + { + "expr": "avg_over_time(praxis_http_request_duration_seconds{quantile=\"0.95\"}[5m])", + "legendFormat": "P95" + } + ], + "title": "P95 Request Latency", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 16, + "x": 8, + "y": 0 + }, + "id": 2, + "targets": [ + { + "expr": "sum(rate(praxis_http_requests_total[5m]))", + "legendFormat": "Requests/sec" + } + ], + "title": "Throughput", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "noValue": "No AI metrics yet" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 3, + "targets": [ + { + "expr": "sum(rate(praxis_ai_prompt_tokens_total[5m])) + sum(rate(praxis_ai_generation_tokens_total[5m]))", + "legendFormat": "Tokens/sec" + } + ], + "title": "Tokens/sec (requires ai#92)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "unit": "currencyUSD", + "noValue": "No AI metrics yet", + "thresholds": { + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 10 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 4, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ] + } + }, + "targets": [ + { + "expr": "sum(increase(praxis_ai_prompt_tokens_total[1h])) * 0.003/1000 + sum(increase(praxis_ai_generation_tokens_total[1h])) * 0.015/1000", + "legendFormat": "Hourly Cost" + } + ], + "title": "Estimated Hourly Cost (requires ai#92)", + "type": "stat" + } + ], + "schemaVersion": 39, + "tags": [ + "praxis", + "ai" + ], + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Praxis AI/LLM Golden Signals", + "uid": "praxis-ai-golden-signals" +} diff --git a/demos/otel-benchmark/stacks/observability/dashboards/praxis-benchmark.json b/demos/otel-benchmark/stacks/observability/dashboards/praxis-benchmark.json new file mode 100644 index 0000000..a4f6cf1 --- /dev/null +++ b/demos/otel-benchmark/stacks/observability/dashboards/praxis-benchmark.json @@ -0,0 +1,59 @@ +{ + "editable": true, + "graphTooltip": 1, + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "id": 1, + "targets": [{ "expr": "sum by (pod) (rate(container_cpu_usage_seconds_total{pod=~\"praxis-proxy.*\"}[1m]))", "legendFormat": "{{pod}}" }], + "title": "Praxis CPU Usage", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "bytes" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "id": 2, + "targets": [{ "expr": "container_memory_working_set_bytes{pod=~\"praxis-proxy.*\", container=\"praxis-proxy\"}", "legendFormat": "{{pod}}" }], + "title": "Praxis Memory Usage", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "id": 3, + "targets": [{ "expr": "sum by (pod) (rate(container_cpu_usage_seconds_total{pod=~\"otel-collector.*\"}[1m]))", "legendFormat": "{{pod}}" }], + "title": "OTel Collector CPU", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "id": 4, + "targets": [{ "expr": "sum(rate(praxis_http_requests_total[1m]))", "legendFormat": "RPS" }], + "title": "Request Rate (1m)", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 }, + "id": 5, + "targets": [ + { "expr": "praxis_http_request_duration_seconds{quantile=\"0.5\"}", "legendFormat": "P50" }, + { "expr": "praxis_http_request_duration_seconds{quantile=\"0.99\"}", "legendFormat": "P99" } + ], + "title": "Latency During Benchmark", + "type": "timeseries" + } + ], + "schemaVersion": 39, + "tags": ["praxis", "benchmark"], + "time": { "from": "now-1h", "to": "now" }, + "title": "Praxis Benchmark Results", + "uid": "praxis-benchmark" +} diff --git a/demos/otel-benchmark/stacks/observability/dashboards/praxis-logs.json b/demos/otel-benchmark/stacks/observability/dashboards/praxis-logs.json new file mode 100644 index 0000000..70db91f --- /dev/null +++ b/demos/otel-benchmark/stacks/observability/dashboards/praxis-logs.json @@ -0,0 +1,30 @@ +{ + "editable": true, + "graphTooltip": 1, + "panels": [ + { + "datasource": { "type": "loki", "uid": "loki" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 6, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "targets": [{ "expr": "sum(rate({container=\"praxis-proxy\"} [5m]))", "legendFormat": "log rate" }], + "title": "Log Rate", + "type": "timeseries" + }, + { + "datasource": { "type": "loki", "uid": "loki" }, + "fieldConfig": { "defaults": {}, "overrides": [] }, + "gridPos": { "h": 14, "w": 24, "x": 0, "y": 6 }, + "id": 2, + "options": { "cellHeight": "sm", "showHeader": true }, + "targets": [{ "expr": "{container=\"praxis-proxy\"}", "refId": "A" }], + "title": "All Praxis Logs", + "type": "table" + } + ], + "schemaVersion": 39, + "tags": ["praxis", "logs"], + "time": { "from": "now-1h", "to": "now" }, + "title": "Praxis Structured Logs", + "uid": "praxis-logs" +} diff --git a/demos/otel-benchmark/stacks/observability/dashboards/praxis-proxy-overview.json b/demos/otel-benchmark/stacks/observability/dashboards/praxis-proxy-overview.json new file mode 100644 index 0000000..35b3090 --- /dev/null +++ b/demos/otel-benchmark/stacks/observability/dashboards/praxis-proxy-overview.json @@ -0,0 +1,50 @@ +{ + "editable": true, + "graphTooltip": 1, + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "id": 1, + "targets": [{ "expr": "sum(rate(praxis_http_requests_total[5m]))", "legendFormat": "RPS" }], + "title": "Request Rate", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "id": 2, + "targets": [ + { "expr": "praxis_http_request_duration_seconds{quantile=\"0.5\"}", "legendFormat": "P50" }, + { "expr": "praxis_http_request_duration_seconds{quantile=\"0.99\"}", "legendFormat": "P99" } + ], + "title": "Latency", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "id": 3, + "targets": [{ "expr": "sum(rate(praxis_http_requests_total[5m])) by (method)", "legendFormat": "{{method}}" }], + "title": "Requests by Method", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "id": 4, + "targets": [{ "expr": "rate(praxis_http_request_duration_seconds_sum[5m]) / rate(praxis_http_request_duration_seconds_count[5m])", "legendFormat": "Avg" }], + "title": "Average Latency", + "type": "timeseries" + } + ], + "schemaVersion": 39, + "tags": ["praxis"], + "time": { "from": "now-15m", "to": "now" }, + "title": "Praxis Proxy Overview", + "uid": "praxis-proxy-overview" +} diff --git a/demos/otel-benchmark/stacks/observability/dashboards/praxis-traces.json b/demos/otel-benchmark/stacks/observability/dashboards/praxis-traces.json new file mode 100644 index 0000000..1748ce4 --- /dev/null +++ b/demos/otel-benchmark/stacks/observability/dashboards/praxis-traces.json @@ -0,0 +1,97 @@ +{ + "annotations": { "list": [] }, + "editable": true, + "graphTooltip": 1, + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 100, + "title": "Trace Search", + "type": "row" + }, + { + "datasource": { "type": "tempo", "uid": "tempo" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { "h": 12, "w": 24, "x": 0, "y": 1 }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true, + "sortBy": [{ "desc": true, "displayName": "Start time" }] + }, + "targets": [ + { + "refId": "A", + "datasource": { "type": "tempo", "uid": "tempo" }, + "queryType": "nativeSearch", + "serviceName": "praxis", + "limit": 20 + } + ], + "title": "Recent Traces", + "type": "table" + }, + { + "datasource": { "type": "tempo", "uid": "tempo" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { "h": 10, "w": 12, "x": 0, "y": 13 }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true + }, + "targets": [ + { + "refId": "A", + "datasource": { "type": "tempo", "uid": "tempo" }, + "queryType": "nativeSearch", + "serviceName": "praxis", + "search": "status=error", + "limit": 20 + } + ], + "title": "Error Traces", + "type": "table" + }, + { + "datasource": { "type": "tempo", "uid": "tempo" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { "h": 10, "w": 12, "x": 12, "y": 13 }, + "id": 3, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true + }, + "targets": [ + { + "refId": "A", + "datasource": { "type": "tempo", "uid": "tempo" }, + "queryType": "nativeSearch", + "serviceName": "praxis", + "minDuration": "100ms", + "limit": 20 + } + ], + "title": "Slow Traces (>100ms)", + "type": "table" + } + ], + "schemaVersion": 39, + "tags": ["praxis", "traces"], + "time": { "from": "now-1h", "to": "now" }, + "title": "Praxis OTel Traces", + "uid": "praxis-traces" +} diff --git a/demos/otel-benchmark/stacks/observability/manifests/otel-collector.yaml b/demos/otel-benchmark/stacks/observability/manifests/otel-collector.yaml new file mode 100644 index 0000000..452cb67 --- /dev/null +++ b/demos/otel-benchmark/stacks/observability/manifests/otel-collector.yaml @@ -0,0 +1,88 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: otel-collector-config + namespace: otel +data: + config.yaml: | + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + + processors: + batch: + timeout: 5s + send_batch_size: 8192 + + exporters: + debug: + verbosity: basic + otlp/tempo: + endpoint: tempo.monitoring.svc:4317 + tls: + insecure: true + service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug, otlp/tempo] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: otel-collector + namespace: otel +spec: + replicas: 1 + selector: + matchLabels: + app: otel-collector + template: + metadata: + labels: + app: otel-collector + spec: + containers: + - name: collector + image: otel/opentelemetry-collector-contrib:0.108.0 + args: ["--config=/etc/otelcol/config.yaml"] + ports: + - containerPort: 4317 + name: otlp-grpc + - containerPort: 4318 + name: otlp-http + volumeMounts: + - name: config + mountPath: /etc/otelcol + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi + volumes: + - name: config + configMap: + name: otel-collector-config +--- +apiVersion: v1 +kind: Service +metadata: + name: otel-collector + namespace: otel +spec: + selector: + app: otel-collector + ports: + - port: 4317 + targetPort: 4317 + name: otlp-grpc + - port: 4318 + targetPort: 4318 + name: otlp-http diff --git a/deny.toml b/deny.toml index 0664f96..7a4f5fe 100644 --- a/deny.toml +++ b/deny.toml @@ -43,4 +43,6 @@ unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] # praxis-ai crates are publish = false and only consumable as git dependencies; # praxis-experimental-server pins them to an exact rev. -allow-git = ["https://github.com/praxis-proxy/ai"] +# praxis-proxy/ai: praxis-ai crates are publish = false (git only). +# praxis-proxy/praxis: patched to main for otel features not yet in v0.5.3. +allow-git = ["https://github.com/praxis-proxy/ai", "https://github.com/praxis-proxy/praxis"]