diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index fb38b4d..d733ad3 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -83,7 +83,7 @@ jobs: shell: bash env: PACT_BROKER_BASE_URL: http://localhost:9292 - BIN: target/${{ inputs.target }}/release/pact + BIN: target/${{ inputs.target }}/release/pact --otel-exporter stdout --enable-otel-traces --enable-otel --log-level info - name: test pact-cli with locally running Pact Broker (Windows) if: ${{ runner.os == 'Windows' && inputs.target != 'aarch64-pc-windows-msvc' }} run: ./run.ps1 diff --git a/Cargo.lock b/Cargo.lock index f2ba524..9ea1d86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1611,17 +1611,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -2027,7 +2016,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.9.2", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2135,9 +2124,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags 2.9.4", "cfg-if", @@ -2176,9 +2165,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -2187,6 +2176,138 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" +dependencies = [ + "opentelemetry 0.31.0", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http 1.3.1", + "opentelemetry 0.31.0", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +dependencies = [ + "http 1.3.1", + "opentelemetry 0.31.0", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk 0.31.0", + "prost 0.14.1", + "reqwest", + "thiserror 2.0.16", + "tokio", + "tonic 0.14.2", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", + "prost 0.14.1", + "tonic 0.14.2", + "tonic-prost", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" + +[[package]] +name = "opentelemetry-stdout" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811" +dependencies = [ + "chrono", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.30.0", + "percent-encoding", + "rand 0.9.2", + "thiserror 2.0.16", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.31.0", + "percent-encoding", + "rand 0.9.2", + "thiserror 2.0.16", +] + [[package]] name = "os_info" version = "3.12.0" @@ -2217,21 +2338,34 @@ dependencies = [ "clap_complete", "home", "indexmap 1.9.3", + "opentelemetry 0.31.0", + "opentelemetry-appender-tracing", + "opentelemetry-http", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry-stdout", + "opentelemetry_sdk 0.31.0", "pact-broker-cli", "pact-plugin-cli", "pact-stub-server", "pact_mock_server_cli", "pact_verifier_cli", + "reqwest-tracing", + "tokio", + "tracing", + "tracing-opentelemetry 0.32.0", + "tracing-subscriber", "trycmd", ] [[package]] name = "pact-broker-cli" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c7cd07df42619a3922cdd5246fa134aaf90ebad538030c34641b7c92a6276" +checksum = "12adb353d2c189857247b1ea0e13735c86b7bbaa0792eaf812b2b8c4113977db" dependencies = [ "anyhow", + "async-trait", "base64 0.22.1", "clap", "clap_complete", @@ -2239,25 +2373,33 @@ dependencies = [ "console 0.16.1", "futures", "glob", + "http 1.3.1", "itertools 0.14.0", - "lazy_static", "log", "maplit", - "md5 0.7.0", "mime", + "opentelemetry 0.31.0", + "opentelemetry-appender-tracing", + "opentelemetry-http", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry-stdout", + "opentelemetry_sdk 0.31.0", "pact_models", "regex", "reqwest", + "reqwest-middleware", + "reqwest-tracing", "serde", "serde_json", "serde_with", - "simplelog", "thiserror 2.0.16", "tokio", "tracing", "tracing-appender", "tracing-core", "tracing-log", + "tracing-opentelemetry 0.32.0", "tracing-subscriber", "url", "urlencoding", @@ -2310,7 +2452,7 @@ dependencies = [ "md5 0.7.0", "os_info", "pact_models", - "prost", + "prost 0.13.5", "prost-types", "regex", "reqwest", @@ -2322,7 +2464,7 @@ dependencies = [ "tar", "tokio", "toml", - "tonic", + "tonic 0.13.1", "tonic-build", "tracing", "tracing-core", @@ -2855,7 +2997,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive 0.14.1", ] [[package]] @@ -2871,7 +3023,7 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", + "prost 0.13.5", "prost-types", "regex", "syn 2.0.106", @@ -2891,13 +3043,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "prost-types" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost", + "prost 0.13.5", ] [[package]] @@ -3176,9 +3341,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "async-compression", "base64 0.22.1", @@ -3220,6 +3385,39 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http 1.3.1", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-tracing" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70ea85f131b2ee9874f0b160ac5976f8af75f3c9badfe0d955880257d10bd83" +dependencies = [ + "anyhow", + "async-trait", + "getrandom 0.2.12", + "http 1.3.1", + "matchit", + "opentelemetry 0.30.0", + "reqwest", + "reqwest-middleware", + "tracing", + "tracing-opentelemetry 0.31.0", +] + [[package]] name = "ring" version = "0.17.8" @@ -3401,11 +3599,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3516,9 +3714,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ "base64 0.22.1", "chrono", @@ -3527,8 +3725,7 @@ dependencies = [ "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", - "serde", - "serde_derive", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -3536,9 +3733,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ "darling", "proc-macro2", @@ -3631,17 +3828,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "simplelog" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" -dependencies = [ - "log", - "termcolor", - "time", -] - [[package]] name = "siphasher" version = "1.0.1" @@ -3886,15 +4072,6 @@ dependencies = [ "windows-sys 0.61.1", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.15.2" @@ -4016,29 +4193,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio 1.0.4", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -4189,7 +4363,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "socket2 0.5.6", "tokio", "tokio-stream", @@ -4199,6 +4373,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tonic-build" version = "0.13.1" @@ -4213,6 +4415,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost 0.14.1", + "tonic 0.14.2", +] + [[package]] name = "tower" version = "0.4.13" @@ -4369,6 +4582,43 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry 0.30.0", + "opentelemetry_sdk 0.30.0", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" +dependencies = [ + "js-sys", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", + "rustversion", + "smallvec", + "thiserror 2.0.16", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" diff --git a/Cargo.toml b/Cargo.toml index d985338..7d60aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,12 +31,30 @@ home = "0.5.5" # pact-plugin-cli = { version = "*", path = "/Users/saf/dev/pact-foundation/pact-plugins/cli"} # pact_verifier_cli = { version = "*", path = "/Users/saf/dev/pact-foundation/pact-reference/rust/pact_verifier_cli"} # pact-broker-cli = { version = "*", path = "/Users/saf/dev/pact-foundation/pact-broker-cli"} -# pact-broker-cli = { version = "*", git = "https://github.com/pact-foundation/pact-broker-cli", branch = "main"} +# pact-broker-cli = { version = "*", git = "https://github.com/pact-foundation/pact-broker-cli", branch = "feat/observability"} pact-stub-server = { version = "*", git = "https://github.com/YOU54F/pact-stub-server.git", branch = "feat/cli_as_lib"} pact_mock_server_cli = { version = "2.0.0-beta.1", git = "https://github.com/YOU54F/pact-core-mock-server.git", branch = "feat/add-pact-specification-option-to-cli"} pact-plugin-cli = { version = "*", git = "https://github.com/pact-foundation/pact-plugins", branch = "feat/cli_as_lib"} pact_verifier_cli = { version = "*", git = "https://github.com/YOU54F/pact-reference", branch = "feat/cli_as_lib"} -pact-broker-cli = { version = "0.2.1" } +pact-broker-cli = { version = "0.3.0" } + +## Otel + +tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "time"] } +opentelemetry = "0.31.0" +opentelemetry-http = "0.31.0" +opentelemetry_sdk = "0.31.0" +opentelemetry-stdout = { version = "0.31.0", features = ["trace", "logs"] } +opentelemetry-otlp = { version = "0.31.0", features = ["trace", "logs", "tls", "tls-roots"] } +opentelemetry-semantic-conventions = "0.31.0" +opentelemetry-appender-tracing = "0.31.0" +tracing = { version= "0.1.41", features = ["std"]} +tracing-subscriber = { version = "0.3.20", features = ["env-filter","registry", "std", "fmt"] } +tracing-opentelemetry = "0.32.0" +reqwest-tracing = { version = "0.5.8", features = ["tracing-opentelemetry_0_31_pkg", "opentelemetry_0_30"] } + +# reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] } +# tracing-opentelemetry = "0.31.0" [dev-dependencies] trycmd = "0.15.0" diff --git a/README.md b/README.md index 626600f..1354a6e 100644 --- a/README.md +++ b/README.md @@ -131,8 +131,22 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --log-level Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] - -h, --help Print help + --enable-otel + Enable OpenTelemetry tracing + --enable-otel-logs + Enable OpenTelemetry logging + --enable-otel-traces + Enable OpenTelemetry traces + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] + -h, --help + Print help ``` @@ -174,10 +188,26 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -o, --output Value must be one of ["json", "text", "table", "pretty"] [default: text] [possible values: json, text, table, pretty] - --log-level Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] - -h, --help Print help - -V, --version Print version + --enable-otel + Enable OpenTelemetry tracing + -o, --output + Value must be one of ["json", "text", "table", "pretty"] [default: text] [possible values: json, text, table, pretty] + --enable-otel-logs + Enable OpenTelemetry logging + --enable-otel-traces + Enable OpenTelemetry traces + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] + -h, --help + Print help + -V, --version + Print version ``` @@ -194,8 +224,22 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --log-level Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] - -h, --help Print help + --enable-otel + Enable OpenTelemetry tracing + --enable-otel-logs + Enable OpenTelemetry logging + --enable-otel-traces + Enable OpenTelemetry traces + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] + -h, --help + Print help ``` @@ -218,12 +262,30 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -y, --yes Automatically answer Yes for all prompts - -d, --debug Enable debug level logs - --log-level Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] - -t, --trace Enable trace level logs - -v, --version Print CLI version - -h, --help Print help + --enable-otel + Enable OpenTelemetry tracing + -y, --yes + Automatically answer Yes for all prompts + -d, --debug + Enable debug level logs + --enable-otel-logs + Enable OpenTelemetry logging + --enable-otel-traces + Enable OpenTelemetry traces + -t, --trace + Enable trace level logs + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] + -v, --version + Print CLI version + -h, --help + Print help ``` @@ -245,14 +307,34 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --help Print help and exit - --log-level Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] - -v, --version Print version information and exit - -p, --port port the master mock server runs on (defaults to 8080) - -h, --host hostname the master mock server runs on (defaults to localhost) - -l, --loglevel Log level for mock servers to write to the log file (defaults to info) [possible values: error, warn, info, debug, trace, none] - --no-term-log Turns off using terminal ANSI escape codes - --no-file-log Do not log to an output file + --enable-otel + Enable OpenTelemetry tracing + --help + Print help and exit + --enable-otel-logs + Enable OpenTelemetry logging + -v, --version + Print version information and exit + --enable-otel-traces + Enable OpenTelemetry traces + -p, --port + port the master mock server runs on (defaults to 8080) + -h, --host + hostname the master mock server runs on (defaults to localhost) + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + -l, --loglevel + Log level for mock servers to write to the log file (defaults to info) [possible values: error, warn, info, debug, trace, none] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --no-term-log + Turns off using terminal ANSI escape codes + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] + --no-file-log + Do not log to an output file + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] ``` @@ -265,9 +347,24 @@ Standalone pact verifier for provider pact verification Usage: pact verifier [OPTIONS] Options: - --help Print help and exit - --log-level Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] - -v, --version Print version information and exit + --enable-otel + Enable OpenTelemetry tracing + --help + Print help and exit + --enable-otel-logs + Enable OpenTelemetry logging + -v, --version + Print version information and exit + --enable-otel-traces + Enable OpenTelemetry traces + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] Logging options: -l, --loglevel Log level to emit log events at (defaults to warn) [possible values: error, warn, info, debug, trace, none] @@ -372,22 +469,34 @@ Pact Stub Server 0.6.3 Usage: pact stub [OPTIONS] Options: + --enable-otel + Enable OpenTelemetry tracing -l, --loglevel Log level (defaults to info) [default: info] [possible values: error, warn, info, debug, trace, none] + --enable-otel-logs + Enable OpenTelemetry logging -f, --file Pact file to load (can be repeated) - --log-level - Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] -d, --dir Directory of pact files to load (can be repeated) + --enable-otel-traces + Enable OpenTelemetry traces -e, --extension File extension to use when loading from a directory (default is json) + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] -u, --url URL of pact file to fetch (can be repeated) -b, --broker-url URL of the pact broker to fetch pacts from [env: PACT_BROKER_BASE_URL=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, http) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf] --user User and password to use when fetching pacts from URLS or Pact Broker in user:password form + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] -t, --token Bearer token to use when fetching pacts from URLS or Pact Broker -p, --port @@ -415,6 +524,49 @@ Options: ``` +## Open Telemetry + +The `pact` cli supports native opentelemetry for traces and application logs. + +It is opt-in via `--enable-otel`, you must set + - `--enable-otel-traces` for traces + - `--enable-otel-logs` for logs + - `--log-level` must be set + - `--enable-otlp-exporter` must be set + +By default, `--otel-exporter-endpoint` will route to `http://localhost:4318`. + +```sh +Options: + --enable-otel + Enable OpenTelemetry tracing + --enable-otel-logs + Enable OpenTelemetry logging + --enable-otel-traces + Enable OpenTelemetry traces + --otel-exporter + The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp) [env: OTEL_TRACES_EXPORTER=] + --otel-exporter-endpoint + The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp) [env: OTEL_EXPORTER_OTLP_ENDPOINT=] + --otel-exporter-protocol + The protocol to use for the OTLP exporter (http/protobuf, grpc) [env: OTEL_EXPORTER_OTLP_PROTOCOL=] [default: http] [possible values: http, http/protobuf, grpc] + --log-level + Set the log level (none, off, error, warn, info, debug, trace) [default: off] [possible values: off, none, error, warn, info, debug, trace] +``` + +Standard otel environemnt variables are followed + +```sh +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" +export OTEL_TRACES_EXPORTER="otlp" +export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +``` + +Currently instrumented crates are + +* pact-broker-cli +* pact-cli + ## Ecosystem The ecosystem is built up of multiple different crates, each which serve a different purpose. diff --git a/examples/fail.json b/examples/fail.json index 8dc1490..21a63fc 100644 --- a/examples/fail.json +++ b/examples/fail.json @@ -1,6 +1,6 @@ { "consumer": { - "name": "me" + "name": "mee" }, "provider": { "name": "they" diff --git a/examples/stub_example-rust.sh b/examples/stub_example-rust.sh index 8e9b153..4c47273 100755 --- a/examples/stub_example-rust.sh +++ b/examples/stub_example-rust.sh @@ -19,6 +19,6 @@ echo '' # AFTER SUITE stop mock service # this would be invoked by the test framework -kill -2 $pid +kill -9 $pid while [ kill -0 $pid 2> /dev/null ]; do sleep 0.5; done \ No newline at end of file diff --git a/examples/verifier_example-rust.sh b/examples/verifier_example-rust.sh index 11e427b..adb39f9 100755 --- a/examples/verifier_example-rust.sh +++ b/examples/verifier_example-rust.sh @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -BIN = ENV['BIN'] || 'pact-provider-verifier' +BIN = ENV['BIN'] || 'pact' puts "=> Starting API" pipe = IO.popen("ruby examples/api.rb") diff --git a/run.sh b/run.sh index ea54a3c..931c714 100755 --- a/run.sh +++ b/run.sh @@ -45,14 +45,29 @@ ${BIN} broker create-environment --name name_foo1 ${BIN} broker create-environment --name name_foo2 --display-name display_name_foo ${BIN} broker create-environment --name name_foo3 --display-name display_name_foo --contact-name contact_name_foo ${BIN} broker create-environment --name name_foo4 --display-name display_name_foo --contact-name contact_name_foo --contact-email-address contact.email.address@foo.bar +# remove --enable-otel arg from BIN to test otel functionality +IS_OTEL_ENABLED=$(echo $BIN | grep -- '--enable-otel --log-level info' || true) +if [ -n "$IS_OTEL_ENABLED" ]; then + # Remove '--enable-otel --log-level info' from BIN in a portable way + BIN=$(echo "$BIN" | sed 's/--enable-otel --log-level info//g') +fi export ENV_UUID=$(${BIN} broker create-environment --name name_foo5 --output=id) +if [ -n "$IS_OTEL_ENABLED" ]; then + BIN="$BIN --enable-otel --log-level info" +fi ${BIN} broker describe-environment --uuid $ENV_UUID ${BIN} broker update-environment --uuid $ENV_UUID --name name_foo6 ${BIN} broker update-environment --uuid $ENV_UUID --name name_foo7 --display-name display_name_foo6 ${BIN} broker update-environment --uuid $ENV_UUID --name name_foo8 --contact-name contact_name_foo8 ${BIN} broker update-environment --uuid $ENV_UUID --name name_foo9 --contact-name contact_name_foo9 --contact-email-address contact_name_foo7 ${BIN} broker delete-environment --uuid $ENV_UUID +if [ -n "$IS_OTEL_ENABLED" ]; then + BIN=$(echo "$BIN" | sed 's/--enable-otel --log-level info//g') +fi ${BIN} broker list-environments | awk -F '│' '{print $2}' | sed -n '3,$p' | sed '$d' | awk '{print $1}' | xargs -I {} ${BIN} broker delete-environment --uuid {} +if [ -n "$IS_OTEL_ENABLED" ]; then + BIN="$BIN --enable-otel --log-level info" +fi ${BIN} broker create-environment --name production --production ${BIN} broker publish tests/pacts -r ${BIN} broker publish tests/pacts -a foo --branch bar @@ -66,7 +81,13 @@ ${BIN} broker create-or-update-pacticipant --name foo --main-branch main --repos ${BIN} broker describe-pacticipant --name foo ${BIN} broker list-pacticipants ${BIN} broker create-webhook https://localhost --request POST --contract-published +if [ -n "$IS_OTEL_ENABLED" ]; then + BIN=$(echo "$BIN" | sed 's/--enable-otel --log-level info//g') +fi export WEBHOOK_UUID=$(${BIN} broker create-webhook https://localhost --request POST --contract-published | jq .uuid -r) +if [ -n "$IS_OTEL_ENABLED" ]; then + BIN="$BIN --enable-otel --log-level info" +fi ${BIN} broker create-or-update-webhook https://foo.bar --request POST --uuid $WEBHOOK_UUID --provider-verification-succeeded ${BIN} broker test-webhook --uuid $WEBHOOK_UUID ${BIN} broker create-or-update-version --version foo --pacticipant foo --branch bar --tag baz @@ -77,6 +98,7 @@ ${BIN} broker delete-branch --branch bar --pacticipant foo ${BIN} broker describe-pacticipant --name foo ${BIN} broker generate-uuid +unset PACT_BROKER_BASE_URL ./examples/mock_example-rust.sh ./examples/stub_example-rust.sh ./examples/verifier_example-rust.sh diff --git a/src/bin.rs b/src/bin.rs index 80f429c..e23e8be 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,62 +1,183 @@ mod cli; +use crate::cli::otel::capture_telemetry; +use crate::cli::otel::init_logging; +use crate::cli::pact_broker_docker; +use crate::cli::pact_broker_ruby; use clap::error::ErrorKind; use clap::ArgMatches; use clap_complete::{generate_to, Shell}; - use std::{process::ExitCode, str::FromStr}; +use tracing::{info, span}; -use crate::cli::pact_broker_docker; -use crate::cli::pact_broker_ruby; - -pub fn main() -> Result<(), ExitCode> { +pub fn main() -> ExitCode { let app = cli::build_cli(); - let cloned_app = app.clone(); - match app.clone().try_get_matches() { + let matches = app.clone().try_get_matches(); + + let ( + enable_otel, + enable_otel_logs, + enable_otel_traces, + otel_exporter, + otel_exporter_endpoint, + otel_exporter_protocol, + log_level, + ) = match &matches { + Ok(m) => ( + m.get_flag("enable-otel"), + m.get_flag("enable-otel-logs"), + m.get_flag("enable-otel-traces"), + m.get_one::("otel-exporter").map(|s| { + s.split(',') + .map(|v| v.trim().to_string()) + .collect::>() + }), + m.get_one::("otel-exporter-endpoint"), + m.get_one::("otel-exporter-protocol"), + m.get_one::("log-level") + .and_then(|lvl| lvl.parse::().ok()), + ), + Err(_) => (false, false, false, None, None, None, None), + }; + let otel_config = Some(crate::cli::otel::OtelConfig { + exporter: otel_exporter.map(|v| v.clone()), + endpoint: otel_exporter_endpoint.cloned(), + protocol: otel_exporter_protocol.cloned(), + enable_otel: Some(enable_otel), + enable_traces: Some(enable_otel_traces), + enable_logs: Some(enable_otel_logs), + log_level, + }); + let tracer_provider = init_logging(otel_config.unwrap()); + let _tracer_provider_dropper; + if tracer_provider.is_some() { + let tracer_provider = tracer_provider.unwrap().clone(); + _tracer_provider_dropper = crate::cli::otel::TracerProviderDropper(tracer_provider); + } + tracing::debug!("Starting application"); + let root = span!(tracing::Level::TRACE, "pact-cli", work_units = 2); + let _root_enter = root.enter(); + let res = match matches { Ok(results) => match results.subcommand() { - Some(("broker", args)) | Some(("pactflow", args)) => { - // if args subcommand is docker or standalone, offset to those subcommands + Some(("broker", args)) => { + let broker_span = span!(tracing::Level::INFO, "broker"); + let _broker_enter = broker_span.enter(); let subcommand = args.subcommand_name(); match subcommand { Some("docker") => { + let docker_span = span!(tracing::Level::INFO, "docker"); + let _docker_enter = docker_span.enter(); let docker_args = args.subcommand_matches("docker").unwrap(); - return pact_broker_docker::run(docker_args); + return match pact_broker_docker::run(docker_args) { + Ok(_) => { + capture_telemetry(&std::env::args().collect::>(), 0, None); + ExitCode::SUCCESS + } + Err(code) => { + capture_telemetry(&std::env::args().collect::>(), 1, None); + code + } + }; // return Ok(()); } Some("ruby") => { + let ruby_span = span!(tracing::Level::INFO, "ruby"); + let _ruby_enter = ruby_span.enter(); let standalone_args = args.subcommand_matches("ruby").unwrap(); - pact_broker_ruby::run(standalone_args); - return Ok(()); + let res = pact_broker_ruby::run(standalone_args); + return match res { + Ok(_) => { + capture_telemetry(&std::env::args().collect::>(), 0, None); + ExitCode::SUCCESS + } + Err(err) => { + println!("{}", err); + capture_telemetry( + &std::env::args().collect::>(), + 1, + Some(err.as_str()), + ); + ExitCode::from(1) + } + }; } _ => {} } let raw_args: Vec = std::env::args().collect(); let matches_result = Ok(args.clone()); - pact_broker_cli::handle_matches(&matches_result, Some(raw_args)) + match pact_broker_cli::handle_matches(&matches_result, Some(raw_args)) { + Ok(()) => { + capture_telemetry(&std::env::args().collect::>(), 0, None); + Ok(()) + } + Err(e) => { + capture_telemetry(&std::env::args().collect::>(), 1, None); + Err(e) + } + } + } + Some(("pactflow", args)) => { + let pactflow_span = span!(tracing::Level::INFO, "pactflow"); + let _pactflow_enter = pactflow_span.enter(); + match pact_broker_cli::cli::pactflow_client::run(args, std::env::args().collect()) { + Ok(_) => { + capture_telemetry(&std::env::args().collect::>(), 0, None); + Ok(()) + } + Err(error) => { + capture_telemetry(&std::env::args().collect::>(), error, None); + Err(ExitCode::from(error as u8)) + } + } + } + Some(("stub", args)) => { + let stub_span = span!(tracing::Level::INFO, "stub"); + let _stub_enter = stub_span.enter(); + let res = pact_stub_server_cli::process_stub_command(args); + capture_telemetry(&std::env::args().collect::>(), 0, None); + res + } + Some(("completions", args)) => { + let completions_span = span!(tracing::Level::INFO, "completions"); + let _completions_enter = completions_span.enter(); + let res = generate_completions(args); + capture_telemetry(&std::env::args().collect::>(), 0, None); + res + } + Some(("plugin", args)) => { + let plugin_span = span!(tracing::Level::INFO, "plugin"); + let _plugin_enter = plugin_span.enter(); + let res = pact_plugin_cli::process_plugin_command(args); + capture_telemetry(&std::env::args().collect::>(), 0, None); + res + } + Some(("mock", args)) => { + let mock_span = span!(tracing::Level::INFO, "mock"); + let _mock_enter = mock_span.enter(); + let res = pact_mock_server_cli::process_mock_command(args); + capture_telemetry(&std::env::args().collect::>(), 0, None); + res + } + Some(("verifier", args)) => { + let verifier_span = span!(tracing::Level::INFO, "verifier"); + let _verifier_enter = verifier_span.enter(); + let res = pact_verifier_cli::process_verifier_command(args); + capture_telemetry(&std::env::args().collect::>(), 0, None); + res } - Some(("stub", args)) => pact_stub_server_cli::process_stub_command(args), - Some(("completions", args)) => generate_completions(args), - Some(("plugin", args)) => pact_plugin_cli::process_plugin_command(args), - Some(("mock", args)) => pact_mock_server_cli::process_mock_command(args), - Some(("verifier", args)) => pact_verifier_cli::process_verifier_command(args), _ => { cli::build_cli().print_help().unwrap(); Ok(()) } }, - Err(err) => match err.kind() { ErrorKind::DisplayHelp => { - // let _ = err.print(); err.exit(); } ErrorKind::DisplayVersion => { let error_message = err.render().to_string(); let versions = [ - ( - "pact-verifier", - pact_verifier_cli::print_version as fn(), - ), + ("pact-verifier", pact_verifier_cli::print_version as fn()), ("pact-mock", pact_mock_server_cli::print_version as fn()), ("pact-stub", pact_stub_server_cli::print_version as fn()), ]; @@ -64,14 +185,26 @@ pub fn main() -> Result<(), ExitCode> { if error_message.contains(name) { print_fn(); println!(); - return Ok(()); + return ExitCode::SUCCESS; } } - // let _ = err.print(); err.exit(); } - _ => err.exit(), + + _ => { + capture_telemetry( + &std::env::args().collect::>(), + err.exit_code(), + Some(&err.to_string()), + ); + + err.exit() + } }, + }; + match res { + Ok(_) => ExitCode::SUCCESS, + Err(code) => code, } } diff --git a/src/cli.rs b/src/cli.rs index e729a35..3e2c1d6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,15 +1,17 @@ -use clap::{command, Arg, Command, CommandFactory}; +use clap::{Arg, Command, CommandFactory}; use crate::cli::{ pact_broker_docker::add_docker_broker_subcommand, pact_broker_ruby::add_ruby_broker_subcommand, }; +pub mod otel; pub mod pact_broker_docker; pub mod pact_broker_ruby; pub fn build_cli() -> Command { let app = Command::new("pact") .about("Pact consolidated CLI - pact_core_mock_server, pact_verifier, pact-stub-server, pact-plugin-cli, pact-broker-cli in a single binary") + .args(add_otel_options_args()) .subcommand( pact_broker_cli::cli::pact_broker_client::add_pact_broker_client_command() .name("broker") @@ -46,3 +48,57 @@ fn add_completions_subcommand() -> Command { .value_parser(clap::builder::NonEmptyStringValueParser::new()) .help("The directory to write the shell completions to, default is the current directory")) } + +fn add_otel_options_args() -> Vec { + vec![ + Arg::new("enable-otel") + .long("enable-otel") + .help("Enable OpenTelemetry tracing") + .global(true) + // .hide(true) + .action(clap::ArgAction::SetTrue), + Arg::new("enable-otel-logs") + .long("enable-otel-logs") + .help("Enable OpenTelemetry logging") + .global(true) + // .hide(true) + .action(clap::ArgAction::SetTrue), + Arg::new("enable-otel-traces") + .long("enable-otel-traces") + .help("Enable OpenTelemetry traces") + .global(true) + // .hide(true) + .action(clap::ArgAction::SetTrue), + Arg::new("otel-exporter") + .long("otel-exporter") + .help("The OpenTelemetry exporter(s) to use, comma separated (stdout, otlp)") + .num_args(1) + .global(true) + // .hide(true) + .env("OTEL_TRACES_EXPORTER") + .value_delimiter(',') + .value_parser(clap::builder::NonEmptyStringValueParser::new()), + Arg::new("otel-exporter-endpoint") + .long("otel-exporter-endpoint") + .help("The endpoint to use for the OTLP exporter (required if --otel-exporter=otlp)") + .num_args(1) + .global(true) + // .hide(true) + .requires_if("otlp", "otel-exporter") + .env("OTEL_EXPORTER_OTLP_ENDPOINT") + .value_parser(clap::builder::NonEmptyStringValueParser::new()), + Arg::new("otel-exporter-protocol") + .long("otel-exporter-protocol") + .help("The protocol to use for the OTLP exporter (http/protobuf, http)") + .num_args(1) + .global(true) + // .hide(true) + .default_value("http") + .requires_if("otlp", "otel-exporter") + .env("OTEL_EXPORTER_OTLP_PROTOCOL") + .value_parser(clap::builder::PossibleValuesParser::new(&[ + "http", + "http/protobuf", + ])), + ] +} diff --git a/src/cli/otel.rs b/src/cli/otel.rs new file mode 100644 index 0000000..f902c1b --- /dev/null +++ b/src/cli/otel.rs @@ -0,0 +1,207 @@ +use opentelemetry::global; +use opentelemetry::trace::TraceContextExt; +use opentelemetry::KeyValue; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +use opentelemetry_otlp::Protocol; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::Resource; +use opentelemetry_sdk::{ + logs::SdkLoggerProvider, propagation::TraceContextPropagator, trace::SdkTracerProvider, +}; +use opentelemetry_stdout::LogExporter; +use std::sync::OnceLock; +use tracing::info; +use tracing::Level; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::Layer; +use tracing_subscriber::Registry; + +#[derive(Debug)] +pub struct OtelConfig { + pub exporter: Option>, + pub endpoint: Option, + pub protocol: Option, + pub enable_otel: Option, + pub enable_traces: Option, + pub enable_logs: Option, + pub log_level: Option, +} + +pub struct TracerProviderDropper(pub opentelemetry_sdk::trace::SdkTracerProvider); + +impl Drop for TracerProviderDropper { + fn drop(&mut self) { + match self.0.force_flush() { + Ok(_) => (), + Err(e) => eprintln!("Failed to flush OpenTelemetry tracing: {e}"), + } + } +} + +fn get_resource() -> Resource { + static RESOURCE: OnceLock = OnceLock::new(); + RESOURCE + .get_or_init(|| { + Resource::builder() + .with_service_name("pact-broker-cli") + .with_attributes(vec![ + KeyValue::new("service.name", env!("CARGO_CRATE_NAME")), + KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), + KeyValue::new( + "service.instance.id", + std::env::var("HOSTNAME").unwrap_or_default(), + ), + KeyValue::new("service.auto.version", env!("CARGO_PKG_VERSION")), + ]) + .build() + }) + .clone() +} + +pub fn init_logging(otel_config: OtelConfig) -> Option { + // If log_level is None, disable logs and tracing + if otel_config.log_level.is_none() { + info!("Log level not set, skipping logging and tracing initialization."); + return None; + } + global::set_text_map_propagator(TraceContextPropagator::new()); + let resource = get_resource(); + + let mut layers: Vec + Send + Sync>> = Vec::new(); + + // Stdout log output if log_level is set + layers.push( + tracing_subscriber::fmt::layer() + .with_level(true) + .with_filter(tracing_subscriber::filter::LevelFilter::from_level( + otel_config.log_level.unwrap(), + )) + .boxed(), + ); + + let mut tracer_provider: Option = None; + + // OTEL trace output + if otel_config.enable_traces.unwrap_or(false) { + let otlp_exporter = if let Some(exporters) = &otel_config.exporter { + if exporters.iter().any(|e| e == "otlp") { + let endpoint = otel_config + .endpoint + .unwrap_or_else(|| "http://localhost:4318".to_string()); + let protocol = otel_config.protocol.unwrap_or_else(|| "http".to_string()); + let exporter = match protocol.as_str() { + "grpc" => opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(endpoint.to_string()) + .build() + .expect("Failed to configure grpc exporter"), + _ => opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpBinary) + .build() + .expect("Failed to configure http exporter"), + }; + Some(exporter) + } else { + None + } + } else { + None + }; + + // Add OTLP exporter as batch if present + tracer_provider = if let Some(exporters_list) = &otel_config.exporter { + let mut builder = SdkTracerProvider::builder().with_resource(resource.clone()); + + if let Some(exporter) = otlp_exporter { + builder = builder.with_batch_exporter(exporter); + } + + // Add stdout exporter as simple if "stdout" is in the exporters list + if exporters_list + .iter() + .any(|e| e == "stdout" || e == "console") + { + println!("Adding stdout exporter for tracing"); + let stdout_exporter = opentelemetry_stdout::SpanExporter::default(); + builder = builder.with_simple_exporter(stdout_exporter); + } + + Some(builder.build()) + } else { + Some( + SdkTracerProvider::builder() + .with_resource(resource.clone()) + .build(), + ) + }; + + if let Some(ref provider) = tracer_provider { + global::set_tracer_provider(provider.clone()); + } + + let tracer = global::tracer("pact-broker-cli"); + + let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); + layers.push(Box::new(telemetry)); + } + + // // OTEL log output + if otel_config.enable_logs.unwrap_or(false) { + let otel_log_stdout_exporter = opentelemetry_stdout::LogExporter::default(); + + let otel_logger_provider = if otel_config.enable_logs.unwrap_or(false) { + let otel_otlp_stdout_exporter = opentelemetry_otlp::LogExporter::builder() + .with_http() + .with_protocol(Protocol::HttpBinary) + .build() + .expect("Failed to create log exporter"); + SdkLoggerProvider::builder() + .with_resource(get_resource()) + .with_simple_exporter(otel_log_stdout_exporter) + .with_batch_exporter(otel_otlp_stdout_exporter) + .build() + } else { + SdkLoggerProvider::builder() + .with_resource(get_resource()) + .with_simple_exporter(otel_log_stdout_exporter) + .build() + }; + let otel_layer = OpenTelemetryTracingBridge::new(&otel_logger_provider); + layers.push(Box::new(otel_layer)); + } + // create a layered subscriber + let subscriber = tracing_subscriber::registry().with(layers); + + if tracing::subscriber::set_global_default(subscriber).is_err() { + info!( + "Global tracing subscriber already set, attaching layers is not supported at runtime." + ); + } + tracer_provider +} + +pub fn capture_telemetry(args: &[String], exit_code: i32, error_message: Option<&str>) { + let span = tracing::Span::current(); + let _enter = span.enter(); + let span_context = span.context(); + let otel_span = span_context.span(); + + if let Some(binary) = args.get(0) { + otel_span.set_attribute(KeyValue::new("binary", binary.clone())); + } + if let Some(command) = args.get(1) { + otel_span.set_attribute(KeyValue::new("command", command.clone())); + } + if let Some(subcommand) = args.get(2) { + otel_span.set_attribute(KeyValue::new("subcommand", subcommand.clone())); + } + if args.len() > 3 { + otel_span.set_attribute(KeyValue::new("args", format!("{:?}", &args[3..]))); + } + otel_span.set_attribute(KeyValue::new("exit_code", exit_code.to_string())); + if let Some(message) = error_message { + otel_span.set_attribute(KeyValue::new("error_message", message.to_string())); + } +} diff --git a/src/cli/pact_broker_ruby.rs b/src/cli/pact_broker_ruby.rs index 799a56d..349851f 100644 --- a/src/cli/pact_broker_ruby.rs +++ b/src/cli/pact_broker_ruby.rs @@ -1,15 +1,27 @@ +use clap::{Arg, ArgMatches, Command}; use std::{ - env, fs, - io::{Read, Write}, + fs, + io::Read, path::Path, process::{Command as Cmd, ExitStatus}, }; -use clap::{Arg, ArgMatches, Command}; - pub fn add_ruby_broker_subcommand() -> Command { Command::new("ruby") .about("Install & Run the Pact Broker using system Ruby in $HOME/.pact/pact-broker") + .subcommand( + Command::new("install") + .about("Install the Pact Broker") + // add enable-otel command + .arg( + Arg::new("enable-otel") + .short('o') + .long("enable-otel") + .num_args(0) + .action(clap::ArgAction::SetTrue) + .help("Enable OpenTelemetry instrumentation for the Pact Broker"), + ), + ) .subcommand( Command::new("start") .about("Setup and Start the Pact Broker") @@ -20,6 +32,14 @@ pub fn add_ruby_broker_subcommand() -> Command { .num_args(0) .action(clap::ArgAction::SetTrue) .help("Run the Pact Broker in the background"), + ) + .arg( + Arg::new("enable-otel") + .short('o') + .long("enable-otel") + .num_args(0) + .action(clap::ArgAction::SetTrue) + .help("Enable OpenTelemetry instrumentation for the Pact Broker"), ), ) .subcommand(Command::new("stop").about("Stop the Pact Broker")) @@ -68,8 +88,9 @@ fn check_bundler_installed() -> Result<(), String> { } } -fn write_gemfile_and_config(broker_dir: &Path) -> std::io::Result<()> { - let gemfile_content = r#"source 'https://rubygems.org' +fn write_gemfile_and_config(broker_dir: &Path, otel_enabled: bool) -> std::io::Result<()> { + let mut gemfile_content = String::from( + r#"source 'https://rubygems.org' gem 'rake' gem 'pact_broker' @@ -84,88 +105,147 @@ gem "pact-support" # required for ruby 3.4 (removed from std gems) gem "mutex_m" gem "csv" -"#; +"#, + ); + + if otel_enabled { + gemfile_content.push_str( + r#" +gem "opentelemetry-api" +gem "opentelemetry-common" +gem "opentelemetry-sdk" +gem "opentelemetry-instrumentation-rack" +gem "opentelemetry-instrumentation-all" +gem "opentelemetry-exporter-otlp" +"#, + ); + } - let config_ru_content = r#"require 'logger' + let config_ru_content = if otel_enabled { + r#"require_relative 'otel' +require 'logger' require 'sequel' require 'pact_broker' DATABASE_CREDENTIALS = {adapter: "sqlite", database: "pact_broker_database.sqlite3", :encoding => 'utf8'} -# run via one of the following: -# -# $ bundle exec rackup -s thin -# $ bundle exec rackup -s puma -# $ bundle exec rackup -s webrick -# -# Note: if using thin, publishing results will fail with the rust verifier, as it requires the Accept-Charset header -# to be set to utf-8. Use puma or webrick instead, until change proposed/merged in pact-rust +app = PactBroker::App.new do | config | + config.log_stream = "stdout" + config.database_connection = Sequel.connect(DATABASE_CREDENTIALS.merge(:logger => config.logger)) +end + +Rack::PactBroker::OpenTelemetry.setup(self) +run app +"# + } else { + r#"require 'logger' +require 'sequel' +require 'pact_broker' +DATABASE_CREDENTIALS = {adapter: "sqlite", database: "pact_broker_database.sqlite3", :encoding => 'utf8'} app = PactBroker::App.new do | config | config.log_stream = "stdout" - # config.base_urls = "http://localhost:9292 http://127.0.0.1:9292 http://0.0.0.0:9292" - # config.database_url = "sqlite:////tmp/pact_broker_database.sqlite3" config.database_connection = Sequel.connect(DATABASE_CREDENTIALS.merge(:logger => config.logger)) end run app -"#; +"# + }; fs::create_dir_all(broker_dir)?; + + if otel_enabled { + let otel_config_content = r#" +require "opentelemetry/sdk" +require "opentelemetry/exporter/otlp" +require "opentelemetry/instrumentation/rack" +require "opentelemetry/instrumentation/rack/middlewares/stable/event_handler" + +module Rack + module PactBroker + class OpenTelemetry + def self.setup(app_builder = nil) + ::OpenTelemetry::SDK.configure do |c| + c.use "OpenTelemetry::Instrumentation::Rack" + c.service_name = ENV.fetch("OTEL_SERVICE_NAME", "pact_broker-standalone") + end + + if app_builder + app_builder.use ::Rack::Events, [::OpenTelemetry::Instrumentation::Rack::Middlewares::Stable::EventHandler.new] + end + end + + at_exit do + OpenTelemetry.tracer_provider.shutdown if defined?(OpenTelemetry) && OpenTelemetry.respond_to?(:tracer_provider) + end + end + end +end +"#; + fs::write(broker_dir.join("otel.rb"), otel_config_content)?; + } + fs::write(broker_dir.join("Gemfile"), gemfile_content)?; fs::write(broker_dir.join("config.ru"), config_ru_content)?; Ok(()) } -pub fn run(args: &ArgMatches) { - let home_dir = home::home_dir().unwrap_or_else(|| { - println!("Could not determine home directory."); - std::process::exit(1); - }); +pub fn install(otel_enabled: bool) -> Result { + check_ruby_version()?; + check_bundler_installed()?; + let home_dir = home::home_dir().ok_or("Could not determine home directory.")?; + let broker_dir = home_dir.join(".pact/pact-broker"); + + write_gemfile_and_config(&broker_dir, otel_enabled) + .map_err(|e| format!("Failed to write Gemfile/config.ru: {}", e))?; + + println!("🚀 Running bundle install in {}", broker_dir.display()); + let status = Cmd::new("ruby") + .arg("-S") + .arg("bundle") + .arg("install") + .current_dir(&broker_dir) + .status() + .map_err(|_| "Failed to run bundle install".to_string())?; + + if status.success() { + Ok(status) + } else { + Err("⚠️ bundle install failed. Please check your Ruby and Bundler setup.".to_string()) + } +} + +fn check_if_installed(broker_dir: &Path) -> bool { + broker_dir.join("Gemfile").exists() && broker_dir.join("config.ru").exists() +} + +pub fn run(args: &ArgMatches) -> Result<(), String> { + let home_dir = home::home_dir().ok_or("Could not determine home directory.")?; let broker_dir = home_dir.join(".pact/pact-broker"); let pid_file_path = broker_dir.join("broker.pid"); match args.subcommand() { - Some(("start", args)) => { - // Check Ruby version - if let Err(msg) = check_ruby_version() { - println!("⚠️ {}", msg); - println!("Please install Ruby >= 3.1 and ensure it is on your PATH."); - std::process::exit(1); - } - - // check bundler version - if let Err(msg) = check_bundler_installed() { - println!("⚠️ {}", msg); + Some(("install", args)) => { + let otel_enabled = args.get_flag("enable-otel"); + if check_if_installed(&broker_dir) { println!( - "Please install Bundler (gem install bundler) and ensure it is on your PATH." + "🚀 Pact Broker is already installed at {}", + broker_dir.display() ); - std::process::exit(1); - } - - // Write Gemfile and config.ru - if let Err(e) = write_gemfile_and_config(&broker_dir) { - println!("Failed to write Gemfile/config.ru: {}", e); - std::process::exit(1); + return Ok(()); } - - // Run bundle install - println!("🚀 Running bundle install in {}", broker_dir.display()); - let status = Cmd::new("ruby") - .arg("-S") - .arg("bundle") - .arg("install") - .current_dir(&broker_dir) - .status() - .expect("Failed to run bundle install"); - if !status.success() { - println!("⚠️ bundle install failed. Please check your Ruby and Bundler setup."); - std::process::exit(1); + println!("🚀 Installing Pact Broker..."); + install(otel_enabled)?; + println!("🚀 Pact Broker installed at {}", broker_dir.display()); + Ok(()) + } + Some(("start", args)) => { + let otel_enabled = args.get_flag("enable-otel"); + if !check_if_installed(&broker_dir) { + println!("🚀 Pact Broker not found, installing..."); + install(otel_enabled)?; } - - // Prepare to start the broker println!("🚀 Starting Pact Broker with Puma..."); - // Use 'ruby -S bundle' for better cross-platform compatibility let mut child_cmd = Cmd::new("ruby"); child_cmd.arg("-S").arg("bundle"); child_cmd @@ -175,84 +255,80 @@ pub fn run(args: &ArgMatches) { .arg(&pid_file_path) .current_dir(&broker_dir); - if let Ok(mut child) = child_cmd.spawn() { - let pid = child.id(); - println!("🚀 Pact Broker is running on http://localhost:9292"); - println!("🚀 PID: {}", pid); - println!("🚀 PID file: {}", pid_file_path.display()); - let mut pid_file_contents = String::from("unknown"); - while !pid_file_contents.chars().all(char::is_numeric) { + let mut child = child_cmd + .spawn() + .map_err(|_| "Failed to start Pact Broker".to_string())?; + let pid = child.id(); + println!("🚀 Pact Broker is running on http://localhost:9292"); + println!("🚀 PID: {}", pid); + println!("🚀 PID file: {}", pid_file_path.display()); + let mut pid_file_contents = String::from("unknown"); + while !pid_file_contents.chars().all(char::is_numeric) { + std::thread::sleep(std::time::Duration::from_secs(1)); + pid_file_contents = + fs::read_to_string(&pid_file_path).unwrap_or_else(|_| String::from("unknown")); + } + println!("Traveling Broker PID: {}", pid_file_contents); + + let detach = args.get_flag("detach"); + if detach { + println!("🚀 Running in the background"); + return Ok(()); + } else { + while child.try_wait().unwrap().is_none() { std::thread::sleep(std::time::Duration::from_secs(1)); - pid_file_contents = fs::read_to_string(&pid_file_path) - .unwrap_or_else(|_| String::from("unknown")); } - println!("Traveling Broker PID: {}", pid_file_contents); - - // we should support a detach flag to run the broker in the background - let detach = args.get_flag("detach"); - if detach { - println!("🚀 Running in the background"); - std::process::exit(0); - } else { - while child.try_wait().unwrap().is_none() { - std::thread::sleep(std::time::Duration::from_secs(1)); + let _ = child.kill(); + let pid_file = fs::File::open(&pid_file_path); + match pid_file { + Ok(mut file) => { + let mut pid = String::new(); + file.read_to_string(&mut pid).unwrap(); + let pid = pid.trim().parse::().unwrap(); + println!("🚀 Stopping Pact Broker with PID: {}", pid); + #[cfg(windows)] + Cmd::new("taskkill") + .arg("/F") + .arg("/PID") + .arg(pid.to_string()) + .output() + .expect("Failed to stop the process"); } - let _ = child.kill(); - let pid_file = fs::File::open(&pid_file_path); - match pid_file { - Ok(mut file) => { - let mut pid = String::new(); - file.read_to_string(&mut pid).unwrap(); - let pid = pid.trim().parse::().unwrap(); - println!("🚀 Stopping Pact Broker with PID: {}", pid); - #[cfg(windows)] - Cmd::new("taskkill") - .arg("/F") - .arg("/PID") - .arg(pid.to_string()) - .output() - .expect("Failed to stop the process"); - } - Err(_) => { - println!("PID file not found"); - } + Err(_) => { + println!("PID file not found"); } - let _ = fs::remove_file(&pid_file_path); - std::process::exit(0); } - } else { - println!("Failed to start Pact Broker"); - std::process::exit(1); + let _ = fs::remove_file(&pid_file_path); + return Ok(()); } } Some(("stop", _args)) => { - if let Ok(mut file) = fs::File::open(&pid_file_path) { - let mut pid = String::new(); - file.read_to_string(&mut pid).unwrap(); - let pid = pid.trim().parse::().unwrap(); - println!("🚀 Stopping Pact Broker with PID: {}", pid); - #[cfg(windows)] - Cmd::new("taskkill") - .arg("/F") - .arg("/PID") - .arg(pid.to_string()) - .output() - .expect("⚠️ Failed to stop the broker"); - - #[cfg(not(windows))] - Cmd::new("kill") - .arg(pid.to_string()) - .output() - .expect("⚠️ Failed to stop the broker"); - let _ = fs::remove_file(&pid_file_path); - println!("🛑 Pact Broker stopped"); - std::process::exit(0); - } else { - println!("⚠️ Pact Broker is not running"); - std::process::exit(1); - } + let mut file = fs::File::open(&pid_file_path) + .map_err(|_| "⚠️ Pact Broker is not running".to_string())?; + let mut pid = String::new(); + file.read_to_string(&mut pid).unwrap(); + let pid = pid.trim().parse::().unwrap(); + println!("🚀 Stopping Pact Broker with PID: {}", pid); + #[cfg(windows)] + Cmd::new("taskkill") + .arg("/F") + .arg("/PID") + .arg(pid.to_string()) + .output() + .expect("⚠️ Failed to stop the broker"); + + #[cfg(not(windows))] + Cmd::new("kill") + .arg(pid.to_string()) + .output() + .expect("⚠️ Failed to stop the broker"); + let _ = fs::remove_file(&pid_file_path); + println!("🛑 Pact Broker stopped"); + Ok(()) } Some(("remove", _args)) => { + let matches = add_ruby_broker_subcommand().get_matches_from(["ruby", "stop"]); + let _ = run(&matches); if let Ok(metadata) = fs::metadata(&broker_dir) { if metadata.is_dir() { if let Err(err) = fs::remove_dir_all(&broker_dir) { @@ -264,6 +340,7 @@ pub fn run(args: &ArgMatches) { } else { println!("broker_dir {} not found", broker_dir.display()); } + Ok(()) } Some(("info", _args)) => { fn check_directory_exists(directory: &Path) -> bool { @@ -300,9 +377,11 @@ pub fn run(args: &ArgMatches) { let pact_broker_pid_exists = get_pid_from_file(&pid_file_path); println!("Pact broker pid: {:?}", pact_broker_pid_exists); + Ok(()) } _ => { println!("⚠️ No option provided, try running ruby --help"); + Ok(()) } } }