├── .github └── workflows │ ├── check.yml │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src ├── event_formatter.rs ├── google.rs ├── layer.rs ├── lib.rs ├── serializers.rs ├── visitor.rs └── writer.rs └── tests ├── default.rs ├── helpers.rs ├── http_request.rs ├── insert_id.rs ├── labels.rs ├── mocks.rs ├── opentelemetry.rs ├── source_location.rs └── valuable.rs /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Pre-release Checks 2 | on: 3 | push: 4 | branches-ignore: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | workflow_call: 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | RUSTFLAGS: '--cfg tracing_unstable' 14 | RUSTDOCFLAGS: '--cfg docsrs' 15 | 16 | jobs: 17 | fmt: 18 | name: Format 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout sources 22 | uses: actions/checkout@v3 23 | - name: Install stable toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: rustfmt 27 | - name: Run cargo fmt 28 | run: cargo fmt --all -- --check 29 | check: 30 | name: Check 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout sources 34 | uses: actions/checkout@v3 35 | - name: ⚡ Cache 36 | uses: actions/cache@v3 37 | with: 38 | path: | 39 | ~/.cargo/bin/ 40 | ~/.cargo/registry/index/ 41 | ~/.cargo/registry/cache/ 42 | ~/.cargo/git/db/ 43 | target/ 44 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 45 | - name: Install stable toolchain 46 | uses: dtolnay/rust-toolchain@stable 47 | - name: Run cargo check 48 | run: cargo check --all-features 49 | test: 50 | name: Test 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout sources 54 | uses: actions/checkout@v3 55 | - name: ⚡ Cache 56 | uses: actions/cache@v3 57 | with: 58 | path: | 59 | ~/.cargo/bin/ 60 | ~/.cargo/registry/index/ 61 | ~/.cargo/registry/cache/ 62 | ~/.cargo/git/db/ 63 | target/ 64 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} 65 | - name: Install stable toolchain 66 | uses: dtolnay/rust-toolchain@stable 67 | - name: Run cargo test 68 | run: cargo test --all-features 69 | lint: 70 | name: Lint 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Checkout sources 74 | uses: actions/checkout@v3 75 | - name: ⚡ Cache 76 | uses: actions/cache@v3 77 | with: 78 | path: | 79 | ~/.cargo/bin/ 80 | ~/.cargo/registry/index/ 81 | ~/.cargo/registry/cache/ 82 | ~/.cargo/git/db/ 83 | target/ 84 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 85 | - name: Install stable toolchain 86 | uses: dtolnay/rust-toolchain@stable 87 | with: 88 | components: clippy 89 | - name: Run cargo clippy 90 | run: cargo clippy --all-features -- -D warnings 91 | docs: 92 | name: Docs 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Checkout sources 96 | uses: actions/checkout@v3 97 | - name: ⚡ Cache 98 | uses: actions/cache@v3 99 | with: 100 | path: | 101 | ~/.cargo/bin/ 102 | ~/.cargo/registry/index/ 103 | ~/.cargo/registry/cache/ 104 | ~/.cargo/git/db/ 105 | target/ 106 | key: ${{ runner.os }}-cargo-docs-${{ hashFiles('**/Cargo.lock') }} 107 | - name: Install nightly toolchain 108 | uses: dtolnay/rust-toolchain@nightly 109 | - name: Run cargo doc 110 | run: cargo doc --all-features 111 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | 7 | jobs: 8 | check: 9 | uses: nalexpear/tracing-stackdriver/.github/workflows/check.yml@master 10 | secrets: inherit 11 | semver: 12 | needs: check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Check semver 18 | uses: obi1kenobi/cargo-semver-checks-action@v2 19 | with: 20 | feature-group: all-features 21 | publish: 22 | needs: semver 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout sources 26 | uses: actions/checkout@v3 27 | - name: ⚡ Cache 28 | uses: actions/cache@v3 29 | with: 30 | path: | 31 | ~/.cargo/bin/ 32 | ~/.cargo/registry/index/ 33 | ~/.cargo/registry/cache/ 34 | ~/.cargo/git/db/ 35 | target/ 36 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 37 | - name: Install stable toolchain 38 | uses: dtolnay/rust-toolchain@stable 39 | - name: Extract release version 40 | run: echo -n "cargo_version=v$(cargo pkgid | cut -d '#' -f2)" >> $GITHUB_ENV 41 | - name: Check release version 42 | if: '${{ env.cargo_version != github.ref_name }}' 43 | uses: actions/github-script@v3 44 | with: 45 | script: | 46 | core.setFailed('Tagged version ${{ github.ref_name }} does not match Cargo version v${{ env.cargo_version }}') 47 | - name: Publish the new version 48 | run: cargo publish --token ${CRATES_TOKEN} 49 | env: 50 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .cargo 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | dependencies = [ 11 | "lazy_static", 12 | "regex", 13 | ] 14 | 15 | [[package]] 16 | name = "aho-corasick" 17 | version = "1.1.1" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" 20 | dependencies = [ 21 | "memchr", 22 | ] 23 | 24 | [[package]] 25 | name = "async-trait" 26 | version = "0.1.73" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" 29 | dependencies = [ 30 | "proc-macro2", 31 | "quote", 32 | "syn 2.0.37", 33 | ] 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.1.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 40 | 41 | [[package]] 42 | name = "bumpalo" 43 | version = "3.14.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 46 | 47 | [[package]] 48 | name = "bytes" 49 | version = "1.5.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 52 | 53 | [[package]] 54 | name = "cfg-if" 55 | version = "1.0.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 58 | 59 | [[package]] 60 | name = "chrono" 61 | version = "0.4.35" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" 64 | dependencies = [ 65 | "num-traits", 66 | ] 67 | 68 | [[package]] 69 | name = "crossbeam-channel" 70 | version = "0.5.8" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 73 | dependencies = [ 74 | "cfg-if", 75 | "crossbeam-utils", 76 | ] 77 | 78 | [[package]] 79 | name = "crossbeam-utils" 80 | version = "0.8.16" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 83 | dependencies = [ 84 | "cfg-if", 85 | ] 86 | 87 | [[package]] 88 | name = "deranged" 89 | version = "0.3.10" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" 92 | dependencies = [ 93 | "powerfmt", 94 | "serde", 95 | ] 96 | 97 | [[package]] 98 | name = "fnv" 99 | version = "1.0.7" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 102 | 103 | [[package]] 104 | name = "form_urlencoded" 105 | version = "1.2.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 108 | dependencies = [ 109 | "percent-encoding", 110 | ] 111 | 112 | [[package]] 113 | name = "futures-channel" 114 | version = "0.3.28" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 117 | dependencies = [ 118 | "futures-core", 119 | ] 120 | 121 | [[package]] 122 | name = "futures-core" 123 | version = "0.3.28" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 126 | 127 | [[package]] 128 | name = "futures-executor" 129 | version = "0.3.28" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 132 | dependencies = [ 133 | "futures-core", 134 | "futures-task", 135 | "futures-util", 136 | ] 137 | 138 | [[package]] 139 | name = "futures-macro" 140 | version = "0.3.28" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 143 | dependencies = [ 144 | "proc-macro2", 145 | "quote", 146 | "syn 2.0.37", 147 | ] 148 | 149 | [[package]] 150 | name = "futures-sink" 151 | version = "0.3.28" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 154 | 155 | [[package]] 156 | name = "futures-task" 157 | version = "0.3.28" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 160 | 161 | [[package]] 162 | name = "futures-util" 163 | version = "0.3.28" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 166 | dependencies = [ 167 | "futures-core", 168 | "futures-macro", 169 | "futures-sink", 170 | "futures-task", 171 | "pin-project-lite", 172 | "pin-utils", 173 | "slab", 174 | ] 175 | 176 | [[package]] 177 | name = "getrandom" 178 | version = "0.2.10" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 181 | dependencies = [ 182 | "cfg-if", 183 | "libc", 184 | "wasi", 185 | ] 186 | 187 | [[package]] 188 | name = "glob" 189 | version = "0.3.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 192 | 193 | [[package]] 194 | name = "http" 195 | version = "0.2.9" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 198 | dependencies = [ 199 | "bytes", 200 | "fnv", 201 | "itoa", 202 | ] 203 | 204 | [[package]] 205 | name = "idna" 206 | version = "0.5.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 209 | dependencies = [ 210 | "unicode-bidi", 211 | "unicode-normalization", 212 | ] 213 | 214 | [[package]] 215 | name = "itoa" 216 | version = "1.0.9" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 219 | 220 | [[package]] 221 | name = "js-sys" 222 | version = "0.3.64" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 225 | dependencies = [ 226 | "wasm-bindgen", 227 | ] 228 | 229 | [[package]] 230 | name = "lazy_static" 231 | version = "1.4.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 234 | 235 | [[package]] 236 | name = "libc" 237 | version = "0.2.148" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 240 | 241 | [[package]] 242 | name = "log" 243 | version = "0.4.20" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 246 | 247 | [[package]] 248 | name = "memchr" 249 | version = "2.6.3" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 252 | 253 | [[package]] 254 | name = "nu-ansi-term" 255 | version = "0.46.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 258 | dependencies = [ 259 | "overload", 260 | "winapi", 261 | ] 262 | 263 | [[package]] 264 | name = "num-traits" 265 | version = "0.2.16" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 268 | dependencies = [ 269 | "autocfg", 270 | ] 271 | 272 | [[package]] 273 | name = "once_cell" 274 | version = "1.18.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 277 | 278 | [[package]] 279 | name = "opentelemetry" 280 | version = "0.22.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "900d57987be3f2aeb70d385fff9b27fb74c5723cc9a52d904d4f9c807a0667bf" 283 | dependencies = [ 284 | "futures-core", 285 | "futures-sink", 286 | "js-sys", 287 | "once_cell", 288 | "pin-project-lite", 289 | "thiserror", 290 | "urlencoding", 291 | ] 292 | 293 | [[package]] 294 | name = "opentelemetry-stdout" 295 | version = "0.3.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "4bdf28b381f23afcd150afc0b38a4183dd321fc96320c1554752b6b761648f78" 298 | dependencies = [ 299 | "chrono", 300 | "futures-util", 301 | "opentelemetry", 302 | "opentelemetry_sdk", 303 | "ordered-float", 304 | "serde", 305 | "serde_json", 306 | ] 307 | 308 | [[package]] 309 | name = "opentelemetry_sdk" 310 | version = "0.22.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "9e90c7113be649e31e9a0f8b5ee24ed7a16923b322c3c5ab6367469c049d6b7e" 313 | dependencies = [ 314 | "async-trait", 315 | "crossbeam-channel", 316 | "futures-channel", 317 | "futures-executor", 318 | "futures-util", 319 | "glob", 320 | "once_cell", 321 | "opentelemetry", 322 | "ordered-float", 323 | "percent-encoding", 324 | "rand", 325 | "thiserror", 326 | ] 327 | 328 | [[package]] 329 | name = "ordered-float" 330 | version = "4.2.0" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" 333 | dependencies = [ 334 | "num-traits", 335 | ] 336 | 337 | [[package]] 338 | name = "overload" 339 | version = "0.1.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 342 | 343 | [[package]] 344 | name = "percent-encoding" 345 | version = "2.3.1" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 348 | 349 | [[package]] 350 | name = "pin-project-lite" 351 | version = "0.2.13" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 354 | 355 | [[package]] 356 | name = "pin-utils" 357 | version = "0.1.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 360 | 361 | [[package]] 362 | name = "powerfmt" 363 | version = "0.2.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 366 | 367 | [[package]] 368 | name = "ppv-lite86" 369 | version = "0.2.17" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 372 | 373 | [[package]] 374 | name = "proc-macro2" 375 | version = "1.0.67" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 378 | dependencies = [ 379 | "unicode-ident", 380 | ] 381 | 382 | [[package]] 383 | name = "quote" 384 | version = "1.0.33" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 387 | dependencies = [ 388 | "proc-macro2", 389 | ] 390 | 391 | [[package]] 392 | name = "rand" 393 | version = "0.8.5" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 396 | dependencies = [ 397 | "libc", 398 | "rand_chacha", 399 | "rand_core", 400 | ] 401 | 402 | [[package]] 403 | name = "rand_chacha" 404 | version = "0.3.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 407 | dependencies = [ 408 | "ppv-lite86", 409 | "rand_core", 410 | ] 411 | 412 | [[package]] 413 | name = "rand_core" 414 | version = "0.6.4" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 417 | dependencies = [ 418 | "getrandom", 419 | ] 420 | 421 | [[package]] 422 | name = "regex" 423 | version = "1.9.5" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" 426 | dependencies = [ 427 | "aho-corasick", 428 | "memchr", 429 | "regex-automata", 430 | "regex-syntax", 431 | ] 432 | 433 | [[package]] 434 | name = "regex-automata" 435 | version = "0.3.8" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" 438 | dependencies = [ 439 | "aho-corasick", 440 | "memchr", 441 | "regex-syntax", 442 | ] 443 | 444 | [[package]] 445 | name = "regex-syntax" 446 | version = "0.7.5" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 449 | 450 | [[package]] 451 | name = "ryu" 452 | version = "1.0.15" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 455 | 456 | [[package]] 457 | name = "serde" 458 | version = "1.0.193" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 461 | dependencies = [ 462 | "serde_derive", 463 | ] 464 | 465 | [[package]] 466 | name = "serde_derive" 467 | version = "1.0.193" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 470 | dependencies = [ 471 | "proc-macro2", 472 | "quote", 473 | "syn 2.0.37", 474 | ] 475 | 476 | [[package]] 477 | name = "serde_json" 478 | version = "1.0.107" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 481 | dependencies = [ 482 | "itoa", 483 | "ryu", 484 | "serde", 485 | ] 486 | 487 | [[package]] 488 | name = "sharded-slab" 489 | version = "0.1.4" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 492 | dependencies = [ 493 | "lazy_static", 494 | ] 495 | 496 | [[package]] 497 | name = "slab" 498 | version = "0.4.9" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 501 | dependencies = [ 502 | "autocfg", 503 | ] 504 | 505 | [[package]] 506 | name = "smallvec" 507 | version = "1.11.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 510 | 511 | [[package]] 512 | name = "syn" 513 | version = "1.0.109" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 516 | dependencies = [ 517 | "proc-macro2", 518 | "quote", 519 | "unicode-ident", 520 | ] 521 | 522 | [[package]] 523 | name = "syn" 524 | version = "2.0.37" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 527 | dependencies = [ 528 | "proc-macro2", 529 | "quote", 530 | "unicode-ident", 531 | ] 532 | 533 | [[package]] 534 | name = "thiserror" 535 | version = "1.0.48" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" 538 | dependencies = [ 539 | "thiserror-impl", 540 | ] 541 | 542 | [[package]] 543 | name = "thiserror-impl" 544 | version = "1.0.48" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" 547 | dependencies = [ 548 | "proc-macro2", 549 | "quote", 550 | "syn 2.0.37", 551 | ] 552 | 553 | [[package]] 554 | name = "thread_local" 555 | version = "1.1.7" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 558 | dependencies = [ 559 | "cfg-if", 560 | "once_cell", 561 | ] 562 | 563 | [[package]] 564 | name = "time" 565 | version = "0.3.30" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" 568 | dependencies = [ 569 | "deranged", 570 | "itoa", 571 | "powerfmt", 572 | "serde", 573 | "time-core", 574 | "time-macros", 575 | ] 576 | 577 | [[package]] 578 | name = "time-core" 579 | version = "0.1.2" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 582 | 583 | [[package]] 584 | name = "time-macros" 585 | version = "0.2.15" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" 588 | dependencies = [ 589 | "time-core", 590 | ] 591 | 592 | [[package]] 593 | name = "tinyvec" 594 | version = "1.6.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 597 | dependencies = [ 598 | "tinyvec_macros", 599 | ] 600 | 601 | [[package]] 602 | name = "tinyvec_macros" 603 | version = "0.1.1" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 606 | 607 | [[package]] 608 | name = "tracing" 609 | version = "0.1.37" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 612 | dependencies = [ 613 | "cfg-if", 614 | "pin-project-lite", 615 | "tracing-attributes", 616 | "tracing-core", 617 | ] 618 | 619 | [[package]] 620 | name = "tracing-attributes" 621 | version = "0.1.26" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" 624 | dependencies = [ 625 | "proc-macro2", 626 | "quote", 627 | "syn 2.0.37", 628 | ] 629 | 630 | [[package]] 631 | name = "tracing-core" 632 | version = "0.1.31" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 635 | dependencies = [ 636 | "once_cell", 637 | "valuable", 638 | ] 639 | 640 | [[package]] 641 | name = "tracing-log" 642 | version = "0.2.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 645 | dependencies = [ 646 | "log", 647 | "once_cell", 648 | "tracing-core", 649 | ] 650 | 651 | [[package]] 652 | name = "tracing-opentelemetry" 653 | version = "0.23.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "a9be14ba1bbe4ab79e9229f7f89fab8d120b865859f10527f31c033e599d2284" 656 | dependencies = [ 657 | "js-sys", 658 | "once_cell", 659 | "opentelemetry", 660 | "opentelemetry_sdk", 661 | "smallvec", 662 | "tracing", 663 | "tracing-core", 664 | "tracing-log", 665 | "tracing-subscriber", 666 | "web-time", 667 | ] 668 | 669 | [[package]] 670 | name = "tracing-serde" 671 | version = "0.1.3" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" 674 | dependencies = [ 675 | "serde", 676 | "tracing-core", 677 | ] 678 | 679 | [[package]] 680 | name = "tracing-stackdriver" 681 | version = "0.10.0" 682 | dependencies = [ 683 | "Inflector", 684 | "http", 685 | "lazy_static", 686 | "opentelemetry", 687 | "opentelemetry-stdout", 688 | "opentelemetry_sdk", 689 | "rand", 690 | "serde", 691 | "serde_json", 692 | "thiserror", 693 | "time", 694 | "tracing", 695 | "tracing-core", 696 | "tracing-opentelemetry", 697 | "tracing-subscriber", 698 | "url", 699 | "valuable", 700 | "valuable-serde", 701 | ] 702 | 703 | [[package]] 704 | name = "tracing-subscriber" 705 | version = "0.3.18" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 708 | dependencies = [ 709 | "nu-ansi-term", 710 | "serde", 711 | "serde_json", 712 | "sharded-slab", 713 | "smallvec", 714 | "thread_local", 715 | "tracing-core", 716 | "tracing-log", 717 | "tracing-serde", 718 | ] 719 | 720 | [[package]] 721 | name = "unicode-bidi" 722 | version = "0.3.13" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 725 | 726 | [[package]] 727 | name = "unicode-ident" 728 | version = "1.0.12" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 731 | 732 | [[package]] 733 | name = "unicode-normalization" 734 | version = "0.1.22" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 737 | dependencies = [ 738 | "tinyvec", 739 | ] 740 | 741 | [[package]] 742 | name = "url" 743 | version = "2.5.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 746 | dependencies = [ 747 | "form_urlencoded", 748 | "idna", 749 | "percent-encoding", 750 | ] 751 | 752 | [[package]] 753 | name = "urlencoding" 754 | version = "2.1.3" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 757 | 758 | [[package]] 759 | name = "valuable" 760 | version = "0.1.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 763 | dependencies = [ 764 | "valuable-derive", 765 | ] 766 | 767 | [[package]] 768 | name = "valuable-derive" 769 | version = "0.1.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "9d44690c645190cfce32f91a1582281654b2338c6073fa250b0949fd25c55b32" 772 | dependencies = [ 773 | "proc-macro2", 774 | "quote", 775 | "syn 1.0.109", 776 | ] 777 | 778 | [[package]] 779 | name = "valuable-serde" 780 | version = "0.1.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "5285cfff30cdabe26626736a54d989687dd9cab84f51f4048b61d6d0ae8b0907" 783 | dependencies = [ 784 | "serde", 785 | "valuable", 786 | ] 787 | 788 | [[package]] 789 | name = "wasi" 790 | version = "0.11.0+wasi-snapshot-preview1" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 793 | 794 | [[package]] 795 | name = "wasm-bindgen" 796 | version = "0.2.87" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 799 | dependencies = [ 800 | "cfg-if", 801 | "wasm-bindgen-macro", 802 | ] 803 | 804 | [[package]] 805 | name = "wasm-bindgen-backend" 806 | version = "0.2.87" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 809 | dependencies = [ 810 | "bumpalo", 811 | "log", 812 | "once_cell", 813 | "proc-macro2", 814 | "quote", 815 | "syn 2.0.37", 816 | "wasm-bindgen-shared", 817 | ] 818 | 819 | [[package]] 820 | name = "wasm-bindgen-macro" 821 | version = "0.2.87" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 824 | dependencies = [ 825 | "quote", 826 | "wasm-bindgen-macro-support", 827 | ] 828 | 829 | [[package]] 830 | name = "wasm-bindgen-macro-support" 831 | version = "0.2.87" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 834 | dependencies = [ 835 | "proc-macro2", 836 | "quote", 837 | "syn 2.0.37", 838 | "wasm-bindgen-backend", 839 | "wasm-bindgen-shared", 840 | ] 841 | 842 | [[package]] 843 | name = "wasm-bindgen-shared" 844 | version = "0.2.87" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 847 | 848 | [[package]] 849 | name = "web-time" 850 | version = "1.1.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 853 | dependencies = [ 854 | "js-sys", 855 | "wasm-bindgen", 856 | ] 857 | 858 | [[package]] 859 | name = "winapi" 860 | version = "0.3.9" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 863 | dependencies = [ 864 | "winapi-i686-pc-windows-gnu", 865 | "winapi-x86_64-pc-windows-gnu", 866 | ] 867 | 868 | [[package]] 869 | name = "winapi-i686-pc-windows-gnu" 870 | version = "0.4.0" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 873 | 874 | [[package]] 875 | name = "winapi-x86_64-pc-windows-gnu" 876 | version = "0.4.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 879 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-stackdriver" 3 | version = "0.10.0" 4 | authors = ["Alex Pearson "] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/NAlexPear/tracing-stackdriver" 9 | description = "Stackdriver-compatible tracing layer and event formatter" 10 | keywords = ["tracing", "stackdriver", "logging", "google", "gcp"] 11 | 12 | [badges.maintenance] 13 | status = "actively-developed" 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [lib] 20 | doctest = false 21 | 22 | [dependencies] 23 | Inflector = "0.11.4" 24 | serde_json = "1.0.94" 25 | tracing-core = "0.1.22" 26 | thiserror = "1.0.40" 27 | 28 | [dependencies.http] 29 | optional = true 30 | version = "0.2.9" 31 | 32 | [dependencies.opentelemetry] 33 | default-features = false 34 | features = ["trace"] 35 | version = "0.22.0" 36 | optional = true 37 | 38 | [dependencies.serde] 39 | features = ["derive"] 40 | version = "1.0.193" 41 | 42 | [dependencies.time] 43 | default-features = false 44 | features = ["formatting"] 45 | version = "0.3.30" 46 | 47 | [dependencies.tracing-opentelemetry] 48 | version = "0.23.0" 49 | optional = true 50 | 51 | [dependencies.tracing-subscriber] 52 | features = ["json"] 53 | version = "0.3.18" 54 | 55 | [dependencies.url] 56 | optional = true 57 | version = "2.5.0" 58 | 59 | [dependencies.valuable] 60 | optional = true 61 | features = ["derive"] 62 | version = "0.1.0" 63 | 64 | [dependencies.valuable-serde] 65 | optional = true 66 | version = "0.1.0" 67 | 68 | [dev-dependencies] 69 | lazy_static = "1.4.0" 70 | tracing = "0.1.34" 71 | rand = "0.8.5" 72 | opentelemetry_sdk = "0.22.1" 73 | 74 | [dev-dependencies.time] 75 | features = ["serde", "serde-well-known", "formatting"] 76 | version = "0.3.30" 77 | 78 | [dev-dependencies.opentelemetry] 79 | default-features = false 80 | features = ["testing", "trace"] 81 | version = "0.22.0" 82 | 83 | [dev-dependencies.opentelemetry-stdout] 84 | features = ["trace"] 85 | version = "0.3.0" 86 | 87 | [features] 88 | valuable = ["dep:valuable", "valuable-serde", "http", "url"] 89 | opentelemetry = ["dep:opentelemetry", "tracing-opentelemetry"] 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `tracing-stackdriver` 2 | 3 | ![Pre-release Checks](https://github.com/NAlexPear/tracing-stackdriver/actions/workflows/check.yml/badge.svg?branch=master) 4 | ![Crates.io](https://img.shields.io/crates/v/tracing-stackdriver) 5 | 6 | [`tracing`](https://docs.rs/tracing/0.1.13/tracing/) is a scoped, structured logging and diagnostic system based on emitting [`Event`](https://docs.rs/tracing/0.1.13/tracing/#events)s in the context of potentially-nested [`Span`](https://docs.rs/tracing/0.1.13/tracing/#spans)s across asynchronous `await` points. These properties make `tracing` ideal for use with [Google Cloud Operations Suite structured logging](https://cloud.google.com/logging/docs/structured-logging) (formerly Stackdriver). 7 | 8 | This crate provides a [`Layer`](https://docs.rs/tracing-subscriber/0.2.4/tracing_subscriber/fmt/struct.Layer.html) for use with a `tracing` [`Registry`](https://docs.rs/tracing-subscriber/0.2.4/tracing_subscriber/struct.Registry.html) that formats `tracing` Spans and Events into properly-structured JSON for consumption by Google Operations Logging through the [`jsonPayload`](https://cloud.google.com/logging/docs/structured-logging) field. This includes the following behaviors and enhancements: 9 | 10 | 1. `rfc3339`-formatted timestamps for all Events 11 | 2. `severity` (in [`LogSeverity`](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity) format) derived from `tracing` [`Level`](https://docs.rs/tracing/0.1.13/tracing/struct.Level.html) 12 | 3. `target` derived from the Event `target` [`Metadata`](https://docs.rs/tracing/0.1.13/tracing/struct.Metadata.html) 13 | 4. Span `name` and custom fields included under a `span` key 14 | 5. automatic nesting of `http_request.`-prefixed event fields 15 | 6. automatic nesting of `labels.`-prefixed event fields, re-written as a [special field](https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields). 16 | 7. automatic re-writing of `insert_id`s as a [special field](https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields). 17 | 8. automatic camelCase-ing of all field keys (e.g. `field_name` -> `fieldName`, or `field.name` -> `fieldName`) 18 | 9. [`valuable`](https://docs.rs/valuable/latest/valuable/) support, including an `HttpRequest` helper `struct` 19 | 10. [Cloud Trace](https://cloud.google.com/trace) support derived from [OpenTelemetry](https://opentelemetry.io) Span and [Trace IDs](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.trace). 20 | 21 | ### Examples 22 | 23 | #### Basic setup: 24 | 25 | ```rust 26 | use tracing_subscriber::{layer::SubscriberExt, Registry}; 27 | 28 | fn main() { 29 | let stackdriver = tracing_stackdriver::layer(); // writes to std::io::Stdout 30 | let subscriber = Registry::default().with(stackdriver); 31 | 32 | tracing::subscriber::set_global_default(subscriber).expect("Could not set up global logger"); 33 | } 34 | ``` 35 | 36 | #### Custom write location: 37 | 38 | ```rust 39 | use tracing_subscriber::{layer::SubscriberExt, Registry}; 40 | 41 | fn main() { 42 | let make_writer = || std::io::Stderr; 43 | let stackdriver = tracing_stackdriver::layer().with_writer(make_writer); // writes to std::io::Stderr 44 | let subscriber = Registry::default().with(stackdriver); 45 | 46 | tracing::subscriber::set_global_default(subscriber).expect("Could not set up global logger"); 47 | } 48 | ``` 49 | 50 | #### With `httpRequest` fields: 51 | 52 | See all available fields [here](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest). 53 | 54 | ```rust 55 | // requires working global setup (see above examples) 56 | 57 | use hyper::Request; 58 | 59 | fn handle_request(request: Request) { 60 | let method = &request.method(); 61 | let uri = &request.uri(); 62 | 63 | tracing::info!( 64 | http_request.request_method = %method, 65 | http_request.request_url = %uri, 66 | "Request received" 67 | ); 68 | 69 | // jsonPayload formatted as: 70 | // { 71 | // "time": "some-timestamp" 72 | // "severity": "INFO", 73 | // "httpRequest": { 74 | // "requestMethod": "GET", 75 | // "requestUrl": "/some/url/from/request" 76 | // }, 77 | // "message": "Request received" 78 | // } 79 | } 80 | ``` 81 | 82 | #### With `labels` fields: 83 | 84 | A key/value map of stringified labels mapped to the `logging.googleapis.com/labels` [special field](https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields). More information about `labels` can be found [here](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.labels). 85 | 86 | ```rust 87 | // requires working global setup (see above examples) 88 | 89 | fn main() { 90 | tracing::info!( 91 | labels.thread_count = 3, 92 | labels.is_production = true, 93 | labels.note = "A short note", 94 | "Application starting" 95 | ); 96 | 97 | // jsonPayload formatted as: 98 | // { 99 | // "time": "some-timestamp" 100 | // "message": "Application starting", 101 | // "logging.googleapis.com/labels": { 102 | // "threadCount": "3", 103 | // "isProduction": "true", 104 | // "note": "A short note", 105 | // } 106 | // } 107 | } 108 | ``` 109 | 110 | #### With `insert_id` field: 111 | 112 | A stringified `insert_id` mapped to the `logging.googleapis.com/insertId` [special field](https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields). More information about `insertId` can be found [here](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.insert_id). This is an optional field, as the Logging API assigns its own unique identifier to this field if `insert_id` is omitted. 113 | 114 | ```rust 115 | // requires working global setup (see above examples) 116 | 117 | fn main() { 118 | tracing::info!( 119 | insert_id = 1234, 120 | "Application starting" 121 | ); 122 | 123 | // jsonPayload formatted as: 124 | // { 125 | // "time": "some-timestamp" 126 | // "message": "Application starting", 127 | // "logging.googleapis.com/insertId": "1234" 128 | // } 129 | } 130 | ``` 131 | 132 | ### With more specific `LogSeverity` levels: 133 | 134 | Google supports a slightly different set of severity levels than `tracing`. `tracing` levels are automatically mapped to `LogSeverity` levels, but you can customize the level beyond the intersection of `tracing` levels and `LogSeverity` levels by using the provided `LogSeverity` level with a `severity` key. 135 | 136 | ```rust 137 | use tracing_stackdriver::LogSeverity; 138 | 139 | fn main() { 140 | // requires working global setup (see above examples) 141 | 142 | tracing::info!(severity = %LogSeverity::Notice, "Application starting"); 143 | 144 | // jsonPayload formatted as: 145 | // { 146 | // "time": "some-timestamp" 147 | // "severity": "NOTICE", 148 | // "message": "Application starting" 149 | // } 150 | } 151 | ``` 152 | 153 | #### With `valuable` support: 154 | 155 | `tracing_stackdriver` supports deeply-nested structured logging through `tracing`'s [unstable `valuable` support](https://github.com/tokio-rs/tracing/discussions/1906). In addition, `httpRequest` fields can be generated with the `HttpRequest` helper struct exported from this library for better compile-time checking of fields. 156 | 157 | To enable `valuable` support, use the `valuable` feature flag and compile your project with `RUSTFLAGS="--cfg tracing_unstable"`. 158 | 159 | ```rust 160 | 161 | // requires working global setup (see above examples) 162 | 163 | use hyper::Request; 164 | use tracing_stackdriver::HttpRequest; 165 | use valuable::Valuable; 166 | 167 | #[derive(Valuable)] 168 | struct StructuredLog { 169 | service: &'static str, 170 | handler: &'static str 171 | } 172 | 173 | fn handle_request(request: Request) { 174 | let http_request = HttpRequest { 175 | request_method: request.method().into(), 176 | request_url: request.uri().into(), 177 | ..Default::default() 178 | }; 179 | 180 | let structured_log = StructuredLog { 181 | service: "request_handlers", 182 | handler: "handle_request", 183 | }; 184 | 185 | tracing::info!( 186 | http_request = http_request.as_value(), 187 | structured_log = structured_log.as_value(), 188 | "Request received" 189 | ); 190 | 191 | // jsonPayload formatted as: 192 | // { 193 | // "time": "some-timestamp" 194 | // "severity": "INFO", 195 | // "httpRequest": { 196 | // "requestMethod": "GET", 197 | // "requestUrl": "/some/url/from/request" 198 | // }, 199 | // "structuredLog": { 200 | // "service": "request_handlers", 201 | // "handler": "handle_request" 202 | // }, 203 | // "message": "Request received" 204 | // } 205 | } 206 | ``` 207 | 208 | #### With Cloud Trace support: 209 | 210 | `tracing_stackdriver` supports integration with [Cloud Trace](https://cloud.google.com/trace) and [OpenTelemetry](https://opentelemetry.io) via [tracing_opentelemetry](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry) and outputs [special Cloud Trace `LogEntry` fields](https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields) for trace sampling and log correlation. 211 | 212 | To enable Cloud Trace support, you need to enable the `opentelemetry` feature flag and provide a `CloudTraceConfiguration` to the `with_cloud_trace` method of the layer. 213 | 214 | ```rust 215 | use tracing_stackdriver::CloudTraceConfiguration; 216 | 217 | fn main() { 218 | // You may want to configure the `tracing_opentelemetry` layer to suit your needs, 219 | // including the use of an additional tracer or exporter. 220 | // See `tracing_opentelemetry`'s doc for details. 221 | let opentelemetry = tracing_opentelemetry::layer(); 222 | 223 | let stackdriver = tracing_stackdriver::layer() 224 | .with_cloud_trace(CloudTraceConfiguration { project_id: "my-project-id" }); 225 | 226 | let subscriber = tracing_subscriber::Registry::default() 227 | .with(opentelemetry) 228 | .with(stackdriver); 229 | 230 | // set up the root span to trigger Span/Trace ID generation 231 | let root = tracing::info_span!("root"); 232 | let _root = root.enter(); 233 | tracing::info!("Application starting"); 234 | 235 | // jsonPayload formatted as: 236 | // { 237 | // "time": "some-timestamp" 238 | // "severity": "INFO", 239 | // "message": "Application starting", 240 | // "logging.googleapis.com/spanId": "0000000000000000", 241 | // "logging.googleapis.com/trace":"projects/my-project-id/traces/0679686673a" 242 | // } 243 | } 244 | ``` 245 | 246 | #### With Source Locations: 247 | 248 | By default, `tracing_stackdriver` includes the source location of `tracing` events in a special [`SourceLocation` composite field](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation) on the emitted `LogEntry`. This behavior can be configured with the `with_source_location` method of the layer. 249 | 250 | ```rust 251 | fn main() { 252 | // Source Locations are enabled by default, so they must be disabled by setting the configuration 253 | // to "false" using with_source_location() 254 | let stackdriver = tracing_stackdriver::layer().with_source_location(false); 255 | let subscriber = tracing_subscriber::Registry::default().with(stackdriver); 256 | tracing::subscriber::set_global_default(subscriber).expect("Could not set up global logger"); 257 | 258 | // tracing events from this point on will have their source location omitted 259 | } 260 | ``` 261 | -------------------------------------------------------------------------------- /src/event_formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | google::LogSeverity, 3 | serializers::{SerializableContext, SerializableSpan, SourceLocation}, 4 | visitor::Visitor, 5 | writer::WriteAdaptor, 6 | }; 7 | use serde::ser::{SerializeMap, Serializer as _}; 8 | use std::fmt; 9 | use time::{format_description::well_known::Rfc3339, OffsetDateTime}; 10 | use tracing_core::{Event, Subscriber}; 11 | use tracing_subscriber::{ 12 | field::VisitOutput, 13 | fmt::{ 14 | format::{self, JsonFields}, 15 | FmtContext, FormatEvent, 16 | }, 17 | registry::LookupSpan, 18 | }; 19 | 20 | #[derive(Debug, thiserror::Error)] 21 | enum Error { 22 | #[error(transparent)] 23 | Formatting(#[from] fmt::Error), 24 | #[error("JSON serialization error: {0}")] 25 | Serialization(#[from] serde_json::Error), 26 | #[error(transparent)] 27 | Io(#[from] std::io::Error), 28 | #[error("Time formatting error: {0}")] 29 | Time(#[from] time::error::Format), 30 | } 31 | 32 | impl From for fmt::Error { 33 | fn from(_: Error) -> Self { 34 | Self 35 | } 36 | } 37 | 38 | /// Tracing Event formatter for Stackdriver layers 39 | pub struct EventFormatter { 40 | pub(crate) include_source_location: bool, 41 | #[cfg(feature = "opentelemetry")] 42 | pub(crate) cloud_trace_configuration: Option, 43 | } 44 | 45 | impl EventFormatter { 46 | /// Internal event formatting for a given serializer 47 | fn format_event( 48 | &self, 49 | context: &FmtContext, 50 | mut serializer: serde_json::Serializer, 51 | event: &Event, 52 | ) -> Result<(), Error> 53 | where 54 | S: Subscriber + for<'span> LookupSpan<'span>, 55 | { 56 | let time = OffsetDateTime::now_utc().format(&Rfc3339)?; 57 | let meta = event.metadata(); 58 | let severity = LogSeverity::from(meta.level()); 59 | 60 | let span = event 61 | .parent() 62 | .and_then(|id| context.span(id)) 63 | .or_else(|| context.lookup_current()); 64 | 65 | // FIXME: derive an accurate entry count ahead of time 66 | let mut map = serializer.serialize_map(None)?; 67 | 68 | // serialize custom fields 69 | map.serialize_entry("time", &time)?; 70 | map.serialize_entry("target", &meta.target())?; 71 | 72 | if self.include_source_location { 73 | if let Some(file) = meta.file() { 74 | map.serialize_entry( 75 | "logging.googleapis.com/sourceLocation", 76 | &SourceLocation { 77 | file, 78 | line: meta.line(), 79 | }, 80 | )?; 81 | } 82 | } 83 | 84 | // serialize the current span and its leaves 85 | if let Some(span) = span { 86 | map.serialize_entry("span", &SerializableSpan::new(&span))?; 87 | map.serialize_entry("spans", &SerializableContext::new(context))?; 88 | 89 | #[cfg(feature = "opentelemetry")] 90 | if let (Some(crate::CloudTraceConfiguration { project_id }), Some(otel_data)) = ( 91 | self.cloud_trace_configuration.as_ref(), 92 | span.extensions().get::(), 93 | ) { 94 | use opentelemetry::trace::TraceContextExt; 95 | 96 | let builder = &otel_data.builder; 97 | 98 | if let Some(span_id) = builder.span_id { 99 | map.serialize_entry("logging.googleapis.com/spanId", &span_id.to_string())?; 100 | } 101 | 102 | let (trace_id, trace_sampled) = if otel_data.parent_cx.has_active_span() { 103 | let span_ref = otel_data.parent_cx.span(); 104 | let span_context = span_ref.span_context(); 105 | 106 | (Some(span_context.trace_id()), span_context.is_sampled()) 107 | } else { 108 | (builder.trace_id, false) 109 | }; 110 | 111 | if let Some(trace_id) = trace_id { 112 | map.serialize_entry( 113 | "logging.googleapis.com/trace", 114 | &format!("projects/{project_id}/traces/{trace_id}",), 115 | )?; 116 | } 117 | 118 | if trace_sampled { 119 | map.serialize_entry("logging.googleapis.com/trace_sampled", &true)?; 120 | } 121 | } 122 | } 123 | 124 | // serialize the stackdriver-specific fields with a visitor 125 | let mut visitor = Visitor::new(severity, map); 126 | event.record(&mut visitor); 127 | visitor.finish().map_err(Error::from)?; 128 | Ok(()) 129 | } 130 | } 131 | 132 | impl FormatEvent for EventFormatter 133 | where 134 | S: Subscriber + for<'span> LookupSpan<'span>, 135 | { 136 | fn format_event( 137 | &self, 138 | context: &FmtContext, 139 | mut writer: format::Writer, 140 | event: &Event, 141 | ) -> fmt::Result 142 | where 143 | S: Subscriber + for<'span> LookupSpan<'span>, 144 | { 145 | let serializer = serde_json::Serializer::new(WriteAdaptor::new(&mut writer)); 146 | self.format_event(context, serializer, event)?; 147 | writeln!(writer) 148 | } 149 | } 150 | 151 | impl Default for EventFormatter { 152 | fn default() -> Self { 153 | Self { 154 | include_source_location: true, 155 | #[cfg(feature = "opentelemetry")] 156 | cloud_trace_configuration: None, 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/google.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::{convert::Infallible, fmt, str::FromStr}; 3 | use tracing_core::Level; 4 | 5 | /// The severity of the event described in a log entry, expressed as standard severity levels. 6 | /// [See Google's LogSeverity docs here](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity). 7 | #[cfg_attr( 8 | all(tracing_unstable, feature = "valuable"), 9 | derive(valuable::Valuable) 10 | )] 11 | #[derive(Debug, Default, Serialize)] 12 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 13 | pub enum LogSeverity { 14 | /// Log entry has no assigned severity level 15 | #[default] 16 | Default, 17 | /// Debug or trace information 18 | Debug, 19 | /// Routine information, such as ongoing status or performance 20 | Info, 21 | /// Normal but significant events, such as start up, shut down, or a configuration change 22 | Notice, 23 | /// Warning events might cause problems 24 | Warning, 25 | /// Error events are likely to cause problems 26 | Error, 27 | /// Critical events cause more severe problems or outages 28 | Critical, 29 | /// A person must take an action immediately 30 | Alert, 31 | /// One or more systems are unusable 32 | Emergency, 33 | } 34 | 35 | impl fmt::Display for LogSeverity { 36 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | let output = match self { 38 | Self::Default => "DEFAULT", 39 | Self::Debug => "DEBUG", 40 | Self::Info => "INFO", 41 | Self::Notice => "NOTICE", 42 | Self::Warning => "WARNING", 43 | Self::Error => "ERROR", 44 | Self::Critical => "CRITICAL", 45 | Self::Alert => "ALERT", 46 | Self::Emergency => "EMERGENCY", 47 | }; 48 | 49 | formatter.write_str(output) 50 | } 51 | } 52 | 53 | impl From<&Level> for LogSeverity { 54 | fn from(level: &Level) -> Self { 55 | match level { 56 | &Level::DEBUG | &Level::TRACE => Self::Debug, 57 | &Level::INFO => Self::Info, 58 | &Level::WARN => Self::Warning, 59 | &Level::ERROR => Self::Error, 60 | } 61 | } 62 | } 63 | 64 | impl FromStr for LogSeverity { 65 | type Err = Infallible; 66 | 67 | fn from_str(string: &str) -> Result { 68 | let severity = match string.to_lowercase().as_str() { 69 | "debug" | "trace" => Self::Debug, 70 | "info" => Self::Info, 71 | "notice" => Self::Notice, 72 | "warn" | "warning" => Self::Warning, 73 | "error" => Self::Error, 74 | "critical" => Self::Critical, 75 | "alert" => Self::Alert, 76 | "emergency" => Self::Emergency, 77 | _ => Self::Default, 78 | }; 79 | 80 | Ok(severity) 81 | } 82 | } 83 | 84 | impl From for LogSeverity { 85 | fn from(json: serde_json::Value) -> Self { 86 | // handle simple string inputs 87 | if let Some(str) = json.as_str() { 88 | return Self::from_str(str).unwrap_or(Self::Default); 89 | } 90 | 91 | // handle wacky object encoding of Valuable enums 92 | #[cfg(all(tracing_unstable, feature = "valuable"))] 93 | if let Some(map) = json.as_object() { 94 | if let Some(key) = map.keys().next() { 95 | return Self::from_str(key).unwrap_or(Self::Default); 96 | } 97 | } 98 | 99 | Self::Default 100 | } 101 | } 102 | 103 | /// Typechecked HttpRequest structure for stucturally logging information about a request. 104 | /// [See Google's HttpRequest docs here](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest). 105 | #[cfg_attr(docsrs, doc(cfg(feature = "valuable")))] 106 | #[cfg(any(docsrs, all(tracing_unstable, feature = "valuable")))] 107 | #[derive(Default)] 108 | pub struct HttpRequest { 109 | /// Valid HTTP Method for the request (e.g. GET, POST, etc) 110 | pub request_method: Option, 111 | /// URL from the HTTP request 112 | pub request_url: Option, 113 | /// Size of the HTTP request in bytes 114 | pub request_size: Option, 115 | /// Size of the HTTP response in bytes 116 | pub response_size: Option, 117 | /// Valid HTTP StatusCode for the response 118 | pub status: Option, 119 | /// User Agent string of the request 120 | pub user_agent: Option, 121 | /// IP address of the client that issued the request 122 | pub remote_ip: Option, 123 | /// IP address of the server that the request was sent to 124 | pub server_ip: Option, 125 | /// Referer URL of the request, as defined in HTTP/1.1 Header Field Definitions 126 | pub referer: Option, 127 | /// Processing latency on the server, from the time the request was received until the response was sent 128 | pub latency: Option, 129 | /// Whether or not a cache lookup was attempted 130 | pub cache_lookup: Option, 131 | /// Whether or not an entity was served from cache (with or without validation) 132 | pub cache_hit: Option, 133 | /// Whether or not the response was validated with the origin server before being served from cache 134 | pub cache_validated_with_origin_server: Option, 135 | /// Number of HTTP response bytes inserted into cache 136 | pub cache_fill_bytes: Option, 137 | /// Protocol used for the request (e.g. "HTTP/1.1", "HTTP/2", "websocket") 138 | pub protocol: Option, 139 | } 140 | 141 | #[cfg_attr(docsrs, doc(cfg(feature = "valuable")))] 142 | #[cfg(any(docsrs, all(tracing_unstable, feature = "valuable")))] 143 | impl HttpRequest { 144 | /// Generate a new log-able HttpRequest structured log entry 145 | pub fn new() -> Self { 146 | Self::default() 147 | } 148 | } 149 | 150 | #[cfg(all(tracing_unstable, feature = "valuable"))] 151 | static HTTP_REQUEST_FIELDS: &[valuable::NamedField<'static>] = &[ 152 | valuable::NamedField::new("requestMethod"), 153 | valuable::NamedField::new("requestUrl"), 154 | valuable::NamedField::new("requestSize"), 155 | valuable::NamedField::new("responseSize"), 156 | valuable::NamedField::new("status"), 157 | valuable::NamedField::new("userAgent"), 158 | valuable::NamedField::new("remoteIp"), 159 | valuable::NamedField::new("serverIp"), 160 | valuable::NamedField::new("referer"), 161 | valuable::NamedField::new("latency"), 162 | valuable::NamedField::new("cacheLookup"), 163 | valuable::NamedField::new("cacheHit"), 164 | valuable::NamedField::new("cacheValidatedWithOriginServer"), 165 | valuable::NamedField::new("cacheFillBytes"), 166 | valuable::NamedField::new("protocol"), 167 | ]; 168 | 169 | #[cfg_attr(docsrs, doc(cfg(feature = "valuable")))] 170 | #[cfg(any(docsrs, all(tracing_unstable, feature = "valuable")))] 171 | impl valuable::Valuable for HttpRequest { 172 | fn as_value(&self) -> valuable::Value<'_> { 173 | valuable::Value::Structable(self) 174 | } 175 | 176 | fn visit(&self, visit: &mut dyn valuable::Visit) { 177 | let request_method = self 178 | .request_method 179 | .as_ref() 180 | .map(|method| method.to_string()); 181 | let request_url = self.request_url.as_ref().map(|url| url.to_string()); 182 | let status = self.status.map(|status| status.as_u16()); 183 | let user_agent = &self.user_agent; 184 | let remote_ip = self.remote_ip.map(|ip| ip.to_string()); 185 | let server_ip = self.server_ip.map(|ip| ip.to_string()); 186 | let referer = self.referer.as_ref().map(|url| url.to_string()); 187 | let latency = self 188 | .latency 189 | .map(|latency| format!("{}s", latency.as_secs_f32())); 190 | 191 | let (fields, values): (Vec<_>, Vec<_>) = HTTP_REQUEST_FIELDS 192 | .iter() 193 | .zip( 194 | [ 195 | request_method.as_ref().map(valuable::Valuable::as_value), 196 | request_url.as_ref().map(valuable::Valuable::as_value), 197 | self.request_size.as_ref().map(valuable::Valuable::as_value), 198 | self.response_size 199 | .as_ref() 200 | .map(valuable::Valuable::as_value), 201 | status.as_ref().map(valuable::Valuable::as_value), 202 | user_agent.as_ref().map(valuable::Valuable::as_value), 203 | remote_ip.as_ref().map(valuable::Valuable::as_value), 204 | server_ip.as_ref().map(valuable::Valuable::as_value), 205 | referer.as_ref().map(valuable::Valuable::as_value), 206 | latency.as_ref().map(valuable::Valuable::as_value), 207 | self.cache_lookup.as_ref().map(valuable::Valuable::as_value), 208 | self.cache_hit.as_ref().map(valuable::Valuable::as_value), 209 | self.cache_validated_with_origin_server 210 | .as_ref() 211 | .map(valuable::Valuable::as_value), 212 | self.cache_fill_bytes 213 | .as_ref() 214 | .map(valuable::Valuable::as_value), 215 | self.protocol.as_ref().map(valuable::Valuable::as_value), 216 | ] 217 | .iter(), 218 | ) 219 | .filter_map(|(field, value)| value.map(|value| (field, value))) 220 | .unzip(); 221 | 222 | visit.visit_named_fields(&valuable::NamedValues::new(&fields, &values)); 223 | } 224 | } 225 | 226 | #[cfg_attr(docsrs, doc(cfg(feature = "valuable")))] 227 | #[cfg(any(docsrs, all(tracing_unstable, feature = "valuable")))] 228 | impl valuable::Structable for HttpRequest { 229 | fn definition(&self) -> valuable::StructDef<'_> { 230 | valuable::StructDef::new_dynamic("HttpRequest", valuable::Fields::Named(&[])) 231 | } 232 | } 233 | 234 | /// Configuration for projects looking to use the [Cloud Trace](https://cloud.google.com/trace) integration 235 | /// through [trace-specific fields](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.trace) in 236 | /// a LogEntry. 237 | #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))] 238 | #[cfg(any(docsrs, feature = "opentelemetry"))] 239 | #[derive(Clone)] 240 | pub struct CloudTraceConfiguration { 241 | /// Google-provided [Project 242 | /// ID](https://cloud.google.com/resource-manager/docs/creating-managing-projects) for 243 | /// prefixing and identifying collectecd traces. 244 | pub project_id: String, 245 | } 246 | -------------------------------------------------------------------------------- /src/layer.rs: -------------------------------------------------------------------------------- 1 | use crate::event_formatter::EventFormatter; 2 | use std::{fmt, io, ops::Deref}; 3 | use tracing_core::{Event, Subscriber}; 4 | use tracing_subscriber::{ 5 | fmt::{format::JsonFields, MakeWriter}, 6 | registry::LookupSpan, 7 | }; 8 | 9 | #[derive(Debug, thiserror::Error)] 10 | enum Error { 11 | #[error(transparent)] 12 | Formatting(#[from] fmt::Error), 13 | #[error("JSON serialization error: {0}")] 14 | Serialization(#[from] serde_json::Error), 15 | #[error(transparent)] 16 | Io(#[from] std::io::Error), 17 | #[error("Time formatting error: {0}")] 18 | Time(#[from] time::error::Format), 19 | } 20 | 21 | impl From for fmt::Error { 22 | fn from(_: Error) -> Self { 23 | Self 24 | } 25 | } 26 | 27 | /// Create a configurable stackdriver-specific Layer and event formatter 28 | pub fn layer() -> Layer 29 | where 30 | S: Subscriber + for<'span> LookupSpan<'span>, 31 | { 32 | Layer( 33 | tracing_subscriber::fmt::layer() 34 | .json() 35 | .event_format(EventFormatter::default()), 36 | ) 37 | } 38 | 39 | /// A tracing-compatible Layer implementation for Stackdriver 40 | pub struct Layer io::Stdout>( 41 | tracing_subscriber::fmt::Layer, 42 | ) 43 | where 44 | S: Subscriber + for<'span> LookupSpan<'span>; 45 | 46 | impl Layer 47 | where 48 | S: Subscriber + for<'span> LookupSpan<'span>, 49 | W: for<'writer> MakeWriter<'writer> + 'static, 50 | { 51 | /// Sets the MakeWriter that the Layer being built will use to write events. 52 | pub fn with_writer(self, make_writer: M) -> Layer 53 | where 54 | M: for<'writer> MakeWriter<'writer> + 'static, 55 | { 56 | Layer(self.0.with_writer(make_writer)) 57 | } 58 | 59 | /// Configures whether or not Events will include source locations in a special LogEntry field 60 | pub fn with_source_location(self, include_source_location: bool) -> Self { 61 | Self(self.0.map_event_format(|mut event_formatter| { 62 | event_formatter.include_source_location = include_source_location; 63 | event_formatter 64 | })) 65 | } 66 | 67 | /// Configures the Cloud Trace integration with OpenTelemetry through special LogEntry fields 68 | #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))] 69 | #[cfg(any(docsrs, feature = "opentelemetry"))] 70 | pub fn with_cloud_trace(self, configuration: crate::CloudTraceConfiguration) -> Self { 71 | Self(self.0.map_event_format(|mut event_formatter| { 72 | event_formatter.cloud_trace_configuration = Some(configuration); 73 | event_formatter 74 | })) 75 | } 76 | } 77 | 78 | /// Layer trait implementation that delegates to the inner Layer methods 79 | impl tracing_subscriber::layer::Layer for Layer 80 | where 81 | S: Subscriber + for<'span> LookupSpan<'span>, 82 | W: for<'writer> MakeWriter<'writer> + 'static, 83 | { 84 | fn on_new_span( 85 | &self, 86 | attrs: &tracing_core::span::Attributes<'_>, 87 | id: &tracing_core::span::Id, 88 | context: tracing_subscriber::layer::Context<'_, S>, 89 | ) { 90 | self.0.on_new_span(attrs, id, context) 91 | } 92 | 93 | fn on_record( 94 | &self, 95 | span: &tracing_core::span::Id, 96 | values: &tracing_core::span::Record<'_>, 97 | context: tracing_subscriber::layer::Context<'_, S>, 98 | ) { 99 | self.0.on_record(span, values, context) 100 | } 101 | 102 | fn on_enter( 103 | &self, 104 | id: &tracing_core::span::Id, 105 | context: tracing_subscriber::layer::Context<'_, S>, 106 | ) { 107 | self.0.on_enter(id, context) 108 | } 109 | 110 | fn on_exit( 111 | &self, 112 | id: &tracing_core::span::Id, 113 | context: tracing_subscriber::layer::Context<'_, S>, 114 | ) { 115 | self.0.on_exit(id, context) 116 | } 117 | 118 | fn on_close( 119 | &self, 120 | id: tracing_core::span::Id, 121 | context: tracing_subscriber::layer::Context<'_, S>, 122 | ) { 123 | self.0.on_close(id, context) 124 | } 125 | 126 | fn on_event(&self, event: &Event<'_>, context: tracing_subscriber::layer::Context<'_, S>) { 127 | self.0.on_event(event, context) 128 | } 129 | 130 | unsafe fn downcast_raw(&self, id: std::any::TypeId) -> Option<*const ()> { 131 | self.0.downcast_raw(id) 132 | } 133 | } 134 | 135 | impl Deref for Layer 136 | where 137 | S: Subscriber + for<'span> LookupSpan<'span>, 138 | { 139 | type Target = tracing_subscriber::fmt::Layer; 140 | 141 | fn deref(&self) -> &Self::Target { 142 | &self.0 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![cfg_attr(not(test), deny(unused_crate_dependencies))] 3 | #![deny(missing_docs, unreachable_pub)] 4 | #![allow(clippy::needless_doctest_main)] 5 | #![doc = include_str!("../README.md")] 6 | 7 | mod event_formatter; 8 | mod google; 9 | mod layer; 10 | mod serializers; 11 | mod visitor; 12 | mod writer; 13 | 14 | pub use self::google::*; 15 | pub use self::layer::*; 16 | -------------------------------------------------------------------------------- /src/serializers.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::{Serialize, SerializeMap, SerializeSeq}; 2 | use serde_json::Value; 3 | use tracing_core::Subscriber; 4 | use tracing_subscriber::{ 5 | fmt::{format::JsonFields, FmtContext, FormattedFields}, 6 | registry::{LookupSpan, SpanRef}, 7 | }; 8 | 9 | /// Serializable tracing span for nesting formatted event fields 10 | pub(crate) struct SerializableSpan<'a, 'b, S>(&'b SpanRef<'a, S>) 11 | where 12 | S: for<'lookup> LookupSpan<'lookup>; 13 | 14 | impl<'a, 'b, S> SerializableSpan<'a, 'b, S> 15 | where 16 | S: for<'lookup> LookupSpan<'lookup>, 17 | { 18 | pub(crate) fn new(span: &'b SpanRef<'a, S>) -> Self { 19 | Self(span) 20 | } 21 | } 22 | 23 | impl<'a, 'b, S> Serialize for SerializableSpan<'a, 'b, S> 24 | where 25 | S: for<'lookup> LookupSpan<'lookup>, 26 | { 27 | fn serialize(&self, serializer: R) -> Result 28 | where 29 | R: serde::Serializer, 30 | { 31 | let name = self.0.name(); 32 | let extensions = self.0.extensions(); 33 | 34 | let formatted_fields = extensions 35 | .get::>() 36 | .expect("No fields!"); 37 | 38 | let span_length = formatted_fields.fields.len() + 1; 39 | let mut map = serializer.serialize_map(Some(span_length))?; 40 | 41 | match serde_json::from_str::(formatted_fields) { 42 | // handle string escaping "properly" (this should be fixed upstream) 43 | // https://github.com/tokio-rs/tracing/issues/391 44 | Ok(Value::Object(fields)) => { 45 | for (key, value) in fields { 46 | map.serialize_entry(&key, &value)?; 47 | } 48 | } 49 | // these two options should be impossible 50 | Ok(value) => panic!("Invalid value: {}", value), 51 | Err(error) => panic!("Error parsing logs: {}", error), 52 | }; 53 | 54 | map.serialize_entry("name", &name)?; 55 | map.end() 56 | } 57 | } 58 | 59 | /// Serializable tracing context for serializing a collection of spans 60 | pub(crate) struct SerializableContext<'a, 'b, S>(&'b FmtContext<'a, S, JsonFields>) 61 | where 62 | S: Subscriber + for<'lookup> LookupSpan<'lookup>; 63 | 64 | impl<'a, 'b, S> SerializableContext<'a, 'b, S> 65 | where 66 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 67 | { 68 | pub(crate) fn new(context: &'b FmtContext<'a, S, JsonFields>) -> Self { 69 | Self(context) 70 | } 71 | } 72 | 73 | impl<'a, 'b, S> Serialize for SerializableContext<'a, 'b, S> 74 | where 75 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 76 | { 77 | fn serialize(&self, serializer: R) -> Result 78 | where 79 | R: serde::Serializer, 80 | { 81 | let mut list = serializer.serialize_seq(None)?; 82 | 83 | if let Some(leaf_span) = self.0.lookup_current() { 84 | for span in leaf_span.scope().from_root() { 85 | list.serialize_element(&SerializableSpan::new(&span))?; 86 | } 87 | } 88 | 89 | list.end() 90 | } 91 | } 92 | 93 | pub(crate) struct SourceLocation<'a> { 94 | pub(crate) file: &'a str, 95 | pub(crate) line: Option, 96 | } 97 | 98 | impl<'a> Serialize for SourceLocation<'a> { 99 | fn serialize(&self, serializer: R) -> Result 100 | where 101 | R: serde::Serializer, 102 | { 103 | let mut map = serializer.serialize_map(Some(if self.line.is_some() { 2 } else { 1 }))?; 104 | map.serialize_entry("file", self.file)?; 105 | if let Some(line) = self.line { 106 | // Stackdriver expects the line number to be serialised as a string: 107 | // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation 108 | map.serialize_entry("line", &line.to_string())?; 109 | } 110 | map.end() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/visitor.rs: -------------------------------------------------------------------------------- 1 | use crate::google::LogSeverity; 2 | use inflector::Inflector; 3 | use serde::ser::SerializeMap; 4 | use std::{collections::BTreeMap, fmt}; 5 | use tracing_core::Field; 6 | use tracing_subscriber::field::{Visit, VisitOutput}; 7 | 8 | /// Visitor for Stackdriver events that formats custom fields 9 | pub(crate) struct Visitor<'a, S> 10 | where 11 | S: SerializeMap, 12 | { 13 | values: BTreeMap<&'a str, serde_json::Value>, 14 | severity: LogSeverity, 15 | serializer: S, 16 | } 17 | 18 | impl<'a, S> Visitor<'a, S> 19 | where 20 | S: SerializeMap, 21 | { 22 | /// Returns a new default visitor using the provided writer 23 | pub(crate) fn new(severity: LogSeverity, serializer: S) -> Self { 24 | Self { 25 | values: BTreeMap::new(), 26 | severity, 27 | serializer, 28 | } 29 | } 30 | } 31 | 32 | impl<'a, S> VisitOutput for Visitor<'a, S> 33 | where 34 | S: SerializeMap, 35 | { 36 | fn finish(mut self) -> fmt::Result { 37 | let inner = || { 38 | let severity = self 39 | .values 40 | .remove("severity") 41 | .map(LogSeverity::from) 42 | .unwrap_or(self.severity); 43 | 44 | self.serializer.serialize_entry("severity", &severity)?; 45 | 46 | let mut http_request = BTreeMap::new(); 47 | let mut labels = BTreeMap::new(); 48 | 49 | for (key, value) in self.values { 50 | let mut key_segments = key.splitn(2, '.'); 51 | 52 | match (key_segments.next(), key_segments.next()) { 53 | (Some("http_request"), Some(request_key)) => { 54 | http_request.insert(request_key.to_camel_case(), value); 55 | } 56 | (Some("labels"), Some(label_key)) => { 57 | let value = match value { 58 | serde_json::Value::String(value) => value, 59 | _ => value.to_string(), 60 | }; 61 | 62 | labels.insert(label_key.to_camel_case(), value); 63 | } 64 | (Some("insert_id"), None) => { 65 | let value = match value { 66 | serde_json::Value::String(value) => value, 67 | _ => value.to_string(), 68 | }; 69 | 70 | self.serializer 71 | .serialize_entry("logging.googleapis.com/insertId", &value)?; 72 | } 73 | (Some(key), None) => self 74 | .serializer 75 | .serialize_entry(&key.to_camel_case(), &value)?, 76 | _ => self 77 | .serializer 78 | .serialize_entry(&key.to_camel_case(), &value)?, 79 | } 80 | } 81 | 82 | if !http_request.is_empty() { 83 | self.serializer 84 | .serialize_entry("httpRequest", &http_request)?; 85 | } 86 | 87 | if !labels.is_empty() { 88 | self.serializer 89 | .serialize_entry("logging.googleapis.com/labels", &labels)?; 90 | } 91 | 92 | self.serializer.end() 93 | }; 94 | 95 | if inner().is_err() { 96 | Err(fmt::Error) 97 | } else { 98 | Ok(()) 99 | } 100 | } 101 | } 102 | 103 | impl<'a, S> Visit for Visitor<'a, S> 104 | where 105 | S: SerializeMap, 106 | { 107 | fn record_i64(&mut self, field: &Field, value: i64) { 108 | self.values 109 | .insert(field.name(), serde_json::Value::from(value)); 110 | } 111 | 112 | fn record_u64(&mut self, field: &Field, value: u64) { 113 | self.values 114 | .insert(field.name(), serde_json::Value::from(value)); 115 | } 116 | 117 | fn record_bool(&mut self, field: &Field, value: bool) { 118 | self.values 119 | .insert(field.name(), serde_json::Value::from(value)); 120 | } 121 | 122 | fn record_str(&mut self, field: &Field, value: &str) { 123 | self.values 124 | .insert(field.name(), serde_json::Value::from(value)); 125 | } 126 | 127 | fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { 128 | self.values.insert( 129 | field.name(), 130 | serde_json::Value::from(format!("{:?}", value)), 131 | ); 132 | } 133 | 134 | #[cfg(all(tracing_unstable, feature = "valuable"))] 135 | fn record_value(&mut self, field: &Field, value: valuable::Value<'_>) { 136 | let value = serde_json::to_value(valuable_serde::Serializable::new(value)).unwrap(); 137 | 138 | self.values.insert(field.name(), value); 139 | } 140 | } 141 | 142 | impl<'a, S> fmt::Debug for Visitor<'a, S> 143 | where 144 | S: SerializeMap, 145 | { 146 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 147 | formatter 148 | .debug_struct("Visitor") 149 | .field("values", &self.values) 150 | .finish() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/writer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Formatter, Write}, 3 | io, 4 | }; 5 | 6 | /// Utility newtype for converting between fmt::Write and io::Write 7 | // https://docs.rs/tracing-subscriber/latest/src/tracing_subscriber/fmt/writer.rs.html 8 | pub(crate) struct WriteAdaptor<'a> { 9 | fmt_write: &'a mut dyn Write, 10 | } 11 | 12 | impl<'a> WriteAdaptor<'a> { 13 | pub(crate) fn new(fmt_write: &'a mut dyn Write) -> Self { 14 | Self { fmt_write } 15 | } 16 | } 17 | 18 | impl<'a> io::Write for WriteAdaptor<'a> { 19 | fn write(&mut self, buf: &[u8]) -> io::Result { 20 | let s = 21 | std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 22 | 23 | self.fmt_write 24 | .write_str(s) 25 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 26 | 27 | Ok(s.as_bytes().len()) 28 | } 29 | 30 | fn flush(&mut self) -> io::Result<()> { 31 | Ok(()) 32 | } 33 | } 34 | 35 | impl<'a> std::fmt::Debug for WriteAdaptor<'a> { 36 | fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { 37 | formatter.pad("WriteAdaptor { .. }") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/default.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::disallowed_names)] 2 | use std::collections::BTreeMap; 3 | 4 | use helpers::run_with_tracing; 5 | use mocks::{MockDefaultEvent, MockEventWithSpan}; 6 | use serde::Deserialize; 7 | use time::OffsetDateTime; 8 | use tracing_stackdriver::LogSeverity; 9 | 10 | mod helpers; 11 | mod mocks; 12 | 13 | #[test] 14 | fn includes_span() { 15 | let events = run_with_tracing::(|| { 16 | let span = tracing::info_span!("stackdriver_span", foo = "bar"); 17 | let _guard = span.enter(); 18 | tracing::info!("some stackdriver message"); 19 | }) 20 | .expect("Error converting test buffer to JSON"); 21 | 22 | let event = events.first().expect("No event heard"); 23 | assert_eq!(event.span.name, "stackdriver_span"); 24 | assert_eq!(event.span.foo, "bar"); 25 | } 26 | 27 | #[test] 28 | fn includes_correct_custom_fields() { 29 | let start = OffsetDateTime::now_utc(); 30 | 31 | let events = run_with_tracing::( 32 | || tracing::info!(target: "test target", "some stackdriver message"), 33 | ) 34 | .expect("Error converting test buffer to JSON"); 35 | 36 | let event = events.first().expect("No event heard"); 37 | assert!(event.time > start); 38 | assert_eq!(event.target, "test target"); 39 | assert_eq!(event.severity, "INFO"); 40 | } 41 | 42 | #[test] 43 | fn includes_custom_fields_with_dot() { 44 | let events = run_with_tracing::>(|| { 45 | tracing::info!(foo.bar = "value", "message") 46 | }) 47 | .expect("Error converting test buffer to JSON"); 48 | 49 | let event = events.first().expect("No event heard"); 50 | assert_eq!( 51 | event.get("fooBar"), 52 | Some(&serde_json::json!("value")), 53 | "full event: {:?}", 54 | event 55 | ); 56 | } 57 | 58 | #[test] 59 | fn handles_stringly_severity_override() { 60 | let events = run_with_tracing::(|| { 61 | tracing::info!(severity = "notice", "notice me, senpai!") 62 | }) 63 | .expect("Error converting test buffer to JSON"); 64 | 65 | let event = events.first().expect("No event heard"); 66 | assert_eq!(event.severity, "NOTICE"); 67 | } 68 | 69 | #[test] 70 | fn handles_enum_severity_override() { 71 | let events = run_with_tracing::(|| { 72 | tracing::info!( 73 | severity = %LogSeverity::Notice, 74 | "notice me, senpai!" 75 | ) 76 | }) 77 | .expect("Error converting test buffer to JSON"); 78 | 79 | let event = events.first().expect("No event heard"); 80 | assert_eq!(event.severity, "NOTICE"); 81 | } 82 | 83 | #[test] 84 | fn includes_correct_timestamps() { 85 | let mut events = run_with_tracing::(|| { 86 | let span = tracing::info_span!("test span", foo = "bar"); 87 | let _guard = span.enter(); 88 | tracing::info!(target: "first target", "some stackdriver message"); 89 | tracing::info!(target: "second target", "some stackdriver message"); 90 | }) 91 | .expect("Error converting test buffer to JSON") 92 | .into_iter(); 93 | 94 | let first_event = events.next().expect("Error logging first event"); 95 | let second_event = events.next().expect("Error logging second event"); 96 | assert!(first_event.time < second_event.time); 97 | } 98 | 99 | #[derive(Deserialize)] 100 | struct MockEventWithFields { 101 | message: String, 102 | baz: u16, 103 | } 104 | 105 | #[test] 106 | fn includes_flattened_fields() { 107 | let baz = 123; 108 | 109 | let events = 110 | run_with_tracing::(|| tracing::info!(baz, "some stackdriver message")) 111 | .expect("Error converting first test buffer to JSON"); 112 | 113 | let event = events.first().expect("No event heard"); 114 | assert_eq!(event.baz, baz); 115 | assert_eq!(event.message, "some stackdriver message"); 116 | } 117 | -------------------------------------------------------------------------------- /tests/helpers.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use serde::Deserialize; 3 | use std::{ 4 | io, 5 | sync::{Arc, Mutex, TryLockError}, 6 | }; 7 | use tracing_stackdriver::Layer; 8 | use tracing_subscriber::{layer::SubscriberExt, Registry}; 9 | 10 | /// Run a traced callback against the default Layer configuration, 11 | /// deserializing into a collection of a single event type `E`. For deserializing events 12 | /// of more than a single type, use `serde_json::Map`. 13 | pub fn run_with_tracing(callback: impl FnOnce()) -> serde_json::Result> 14 | where 15 | E: for<'a> Deserialize<'a>, 16 | { 17 | run_with_tracing_layer(tracing_stackdriver::layer(), callback) 18 | } 19 | 20 | /// Run a traced callback against a Layer configuration 21 | // FIXME: handle composable layers (a la "with") in run_with_tracing functions 22 | pub fn run_with_tracing_layer( 23 | layer: Layer, 24 | callback: impl FnOnce(), 25 | ) -> serde_json::Result> 26 | where 27 | E: for<'a> Deserialize<'a>, 28 | { 29 | let buffer = Arc::new(Mutex::new(vec![])); 30 | let shared = buffer.clone(); 31 | let make_writer = move || MockWriter(shared.clone()); 32 | let stackdriver: Layer = layer.with_writer(make_writer); 33 | let subscriber = Registry::default().with(stackdriver); 34 | 35 | tracing::subscriber::with_default(subscriber, callback); 36 | 37 | let buffer = buffer 38 | .lock() 39 | .expect("Couldn't get lock on test write target"); 40 | 41 | serde_json::Deserializer::from_slice(&buffer) 42 | .into_iter() 43 | .collect() 44 | } 45 | 46 | // FIXME: make this entirely internal 47 | #[derive(Debug)] 48 | pub struct MockWriter(pub Arc>>); 49 | 50 | impl MockWriter { 51 | pub fn map_err(error: TryLockError) -> io::Error { 52 | match error { 53 | TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock), 54 | TryLockError::Poisoned(_) => io::Error::from(io::ErrorKind::Other), 55 | } 56 | } 57 | } 58 | 59 | impl io::Write for MockWriter { 60 | fn write(&mut self, buffer: &[u8]) -> io::Result { 61 | self.0.try_lock().map_err(Self::map_err)?.write(buffer) 62 | } 63 | 64 | fn flush(&mut self) -> io::Result<()> { 65 | self.0.try_lock().map_err(Self::map_err)?.flush() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/http_request.rs: -------------------------------------------------------------------------------- 1 | use helpers::run_with_tracing; 2 | use mocks::{MockHttpEvent, MockHttpRequest}; 3 | 4 | mod helpers; 5 | mod mocks; 6 | 7 | #[test] 8 | fn nests_http_request() { 9 | let request_method = "GET"; 10 | let latency = "0.23s"; 11 | let remote_ip = "192.168.1.1"; 12 | let status = 200; 13 | 14 | let mock_http_request = MockHttpRequest { 15 | request_method: request_method.to_string(), 16 | latency: latency.to_string(), 17 | remote_ip: remote_ip.to_string(), 18 | status, 19 | }; 20 | 21 | let events = run_with_tracing::(|| { 22 | tracing::info!( 23 | http_request.request_method = &request_method, 24 | http_request.latency = &latency, 25 | http_request.remote_ip = &remote_ip, 26 | http_request.status = &status, 27 | "some stackdriver message" 28 | ) 29 | }) 30 | .expect("Error converting test buffer to JSON"); 31 | 32 | let event = events.first().expect("No event heard"); 33 | assert_eq!(event.http_request, mock_http_request); 34 | } 35 | -------------------------------------------------------------------------------- /tests/insert_id.rs: -------------------------------------------------------------------------------- 1 | use helpers::run_with_tracing; 2 | use mocks::MockDefaultEvent; 3 | 4 | mod helpers; 5 | mod mocks; 6 | 7 | #[test] 8 | fn includes_custom_insert_ids() { 9 | let insert_id = "my-new-event".to_string(); 10 | let events = 11 | run_with_tracing::(|| tracing::info!(insert_id = insert_id, "hello!")) 12 | .expect("Error converting test buffer to JSON"); 13 | 14 | let event = events.first().expect("No event heard"); 15 | assert!(event.insert_id.is_some()); 16 | assert_eq!(event.insert_id, Some(insert_id)); 17 | } 18 | 19 | #[test] 20 | fn stringifies_primitive_insert_id_values() { 21 | let insert_id = 123; 22 | let events = 23 | run_with_tracing::(|| tracing::info!(insert_id = insert_id, "hello!")) 24 | .expect("Error converting test buffer to JSON"); 25 | 26 | let event = events.first().expect("No event heard"); 27 | assert!(event.insert_id.is_some()); 28 | assert_eq!(event.insert_id, Some(insert_id.to_string())); 29 | } 30 | 31 | #[test] 32 | fn omits_insert_id_by_default() { 33 | let events = run_with_tracing::(|| tracing::info!("hello!")) 34 | .expect("Error converting test buffer to JSON"); 35 | 36 | let event = events.first().expect("No event heard"); 37 | assert!(event.insert_id.is_none()); 38 | } 39 | -------------------------------------------------------------------------------- /tests/labels.rs: -------------------------------------------------------------------------------- 1 | use helpers::run_with_tracing; 2 | use mocks::MockDefaultEvent; 3 | use std::collections::BTreeMap; 4 | 5 | mod helpers; 6 | mod mocks; 7 | 8 | #[test] 9 | fn nests_labels() { 10 | let mut labels = BTreeMap::new(); 11 | labels.insert("foo", "bar".to_string()); 12 | labels.insert("baz", "luhrmann".to_string()); 13 | 14 | let events = run_with_tracing::(|| { 15 | tracing::info!( 16 | labels.foo = labels.get("foo"), 17 | labels.baz = labels.get("baz"), 18 | "hello!" 19 | ) 20 | }) 21 | .expect("Error converting test buffer to JSON"); 22 | 23 | let event = events.first().expect("No event heard"); 24 | assert!(event.labels.get("foo").is_some()); 25 | assert_eq!(event.labels.get("foo"), labels.get("foo")); 26 | assert!(event.labels.get("baz").is_some()); 27 | assert_eq!(event.labels.get("baz"), labels.get("baz")); 28 | } 29 | 30 | #[test] 31 | fn stringifies_primitive_label_values() { 32 | let number = 2; 33 | let boolean = false; 34 | let string = "a short note"; 35 | let events = run_with_tracing::(|| { 36 | tracing::info!( 37 | labels.number = number, 38 | labels.boolean = boolean, 39 | labels.string = string, 40 | "hello!" 41 | ) 42 | }) 43 | .expect("Error converting test buffer to JSON"); 44 | 45 | let event = events.first().expect("No event heard"); 46 | assert_eq!(event.labels.get("number"), Some(&number.to_string())); 47 | assert_eq!(event.labels.get("boolean"), Some(&boolean.to_string())); 48 | assert_eq!(event.labels.get("string"), Some(&string.to_string())); 49 | } 50 | 51 | #[test] 52 | fn omits_labels_by_default() { 53 | let events = run_with_tracing::(|| tracing::info!("hello!")) 54 | .expect("Error converting test buffer to JSON"); 55 | 56 | let event = events.first().expect("No event heard"); 57 | assert!(event.labels.is_empty()); 58 | } 59 | -------------------------------------------------------------------------------- /tests/mocks.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::BTreeMap; 3 | use time::OffsetDateTime; 4 | 5 | #[derive(Clone, Debug, Deserialize)] 6 | pub struct MockSourceLocation { 7 | pub file: String, 8 | pub line: String, 9 | } 10 | 11 | #[derive(Clone, Deserialize, Debug)] 12 | pub struct MockDefaultEvent { 13 | #[serde(deserialize_with = "time::serde::rfc3339::deserialize")] 14 | pub time: OffsetDateTime, 15 | pub target: String, 16 | pub severity: String, 17 | #[serde(rename = "logging.googleapis.com/sourceLocation")] 18 | pub source_location: MockSourceLocation, 19 | #[serde(rename = "logging.googleapis.com/labels", default)] 20 | pub labels: BTreeMap, 21 | #[serde(rename = "logging.googleapis.com/insertId", default)] 22 | pub insert_id: Option, 23 | } 24 | 25 | #[derive(Debug, Deserialize)] 26 | pub struct MockSpan { 27 | pub name: String, 28 | pub foo: String, 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | pub struct MockEventWithSpan { 33 | pub span: MockSpan, 34 | } 35 | 36 | #[derive(Debug, Deserialize, PartialEq)] 37 | #[serde(rename_all = "camelCase")] 38 | pub struct MockHttpRequest { 39 | pub request_method: String, 40 | pub latency: String, 41 | pub remote_ip: String, 42 | pub status: u16, 43 | } 44 | 45 | #[derive(Debug, Deserialize)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct MockHttpEvent { 48 | pub http_request: MockHttpRequest, 49 | } 50 | -------------------------------------------------------------------------------- /tests/opentelemetry.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "opentelemetry")] 2 | use helpers::MockWriter; 3 | use lazy_static::lazy_static; 4 | use opentelemetry::{ 5 | testing::trace::TestSpan, 6 | trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState}, 7 | }; 8 | use opentelemetry_sdk::trace::TracerProvider; 9 | use rand::Rng; 10 | use serde::{de::Error, Deserialize, Deserializer}; 11 | use std::{ 12 | fmt::Debug, 13 | sync::{Arc, Mutex}, 14 | }; 15 | use tracing_stackdriver::CloudTraceConfiguration; 16 | use tracing_subscriber::{fmt::MakeWriter, layer::SubscriberExt}; 17 | 18 | mod helpers; 19 | mod mocks; 20 | 21 | static PROJECT_ID: &str = "my_project_123"; 22 | 23 | lazy_static! { 24 | static ref CLOUD_TRACE_CONFIGURATION: CloudTraceConfiguration = CloudTraceConfiguration { 25 | project_id: PROJECT_ID.to_owned(), 26 | }; 27 | 28 | // use a tracer that generates valid span IDs (unlike default NoopTracer) 29 | static ref TRACER: TracerProvider = TracerProvider::builder() 30 | .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) 31 | .build(); 32 | } 33 | 34 | #[derive(Debug, Deserialize)] 35 | struct MockEventWithCloudTraceFields { 36 | #[serde( 37 | rename = "logging.googleapis.com/spanId", 38 | deserialize_with = "from_hex" 39 | )] 40 | span_id: SpanId, 41 | #[serde(rename = "logging.googleapis.com/trace")] 42 | trace_id: String, 43 | #[serde(rename = "logging.googleapis.com/trace_sampled", default)] 44 | trace_sampled: bool, 45 | } 46 | 47 | fn from_hex<'de, D>(deserializer: D) -> Result 48 | where 49 | D: Deserializer<'de>, 50 | { 51 | let hex: &str = Deserialize::deserialize(deserializer)?; 52 | SpanId::from_hex(hex).map_err(D::Error::custom) 53 | } 54 | 55 | fn test_with_tracing(span_id: SpanId, trace_id: TraceId, make_writer: M, callback: impl FnOnce()) 56 | where 57 | M: for<'writer> MakeWriter<'writer> + Sync + Send + 'static, 58 | { 59 | use opentelemetry::trace::TracerProvider as _; 60 | 61 | // generate the tracing subscriber 62 | let subscriber = tracing_subscriber::registry() 63 | .with( 64 | tracing_opentelemetry::layer() 65 | .with_location(false) 66 | .with_threads(false) 67 | .with_tracked_inactivity(false) 68 | .with_tracer(TRACER.tracer("test")), 69 | ) 70 | .with( 71 | tracing_stackdriver::layer() 72 | .with_writer(make_writer) 73 | .with_cloud_trace(CLOUD_TRACE_CONFIGURATION.clone()), 74 | ); 75 | 76 | // generate a context for events 77 | let context = opentelemetry::Context::current_with_span(TestSpan(SpanContext::new( 78 | trace_id, 79 | span_id, 80 | TraceFlags::default(), 81 | false, 82 | TraceState::default(), 83 | ))); 84 | 85 | // attach the tracing context 86 | let _context = context.attach(); 87 | 88 | // run the callback in a tracing context 89 | tracing::subscriber::with_default(subscriber, callback); 90 | } 91 | 92 | #[test] 93 | fn includes_correct_cloud_trace_fields() { 94 | // generate the output buffer 95 | let buffer = Arc::new(Mutex::new(vec![])); 96 | let shared = buffer.clone(); 97 | let make_writer = move || MockWriter(shared.clone()); 98 | 99 | // generate relevant IDs 100 | let mut rng = rand::thread_rng(); 101 | let span_id = SpanId::from_u64(rng.gen()); 102 | let trace_id = TraceId::from_u128(rng.gen()); 103 | 104 | // generate a tracing-based event 105 | test_with_tracing(span_id, trace_id, make_writer, || { 106 | let root = tracing::debug_span!("root"); 107 | let _root = root.enter(); 108 | tracing::debug!("test event"); 109 | }); 110 | 111 | let output: MockEventWithCloudTraceFields = serde_json::from_slice(&buffer.try_lock().unwrap()) 112 | .expect("Error converting test buffer to JSON"); 113 | 114 | // span IDs should NOT be propagated, but generated for each span 115 | assert_ne!( 116 | output.span_id, span_id, 117 | "Span IDs are the same, but should not be" 118 | ); 119 | 120 | // trace ID should be propagated and formatted in Cloud Trace format 121 | assert_eq!( 122 | output.trace_id, 123 | format!("projects/{PROJECT_ID}/traces/{trace_id}"), 124 | "Trace IDs are not compatible", 125 | ); 126 | 127 | // trace sampling should be disabled by default 128 | assert!(!output.trace_sampled) 129 | } 130 | 131 | #[test] 132 | fn handles_nested_spans() { 133 | // generate the output buffer 134 | let buffer = Arc::new(Mutex::new(vec![])); 135 | let shared = buffer.clone(); 136 | let make_writer = move || MockWriter(shared.clone()); 137 | 138 | // generate relevant IDs 139 | let mut rng = rand::thread_rng(); 140 | let span_id = SpanId::from_u64(rng.gen()); 141 | let trace_id = TraceId::from_u128(rng.gen()); 142 | 143 | // generate a set of nested tracing-based events 144 | test_with_tracing(span_id, trace_id, make_writer, || { 145 | let root = tracing::debug_span!("root"); 146 | let _root = root.enter(); 147 | tracing::debug!("top-level test event"); 148 | let inner = tracing::debug_span!("inner"); 149 | let _inner = inner.enter(); 150 | tracing::debug!("inner test event"); 151 | }); 152 | 153 | // parse the newline-separated messages from the test buffer 154 | let raw = &buffer.try_lock().unwrap(); 155 | 156 | let mut messages = raw 157 | .split(|byte| byte == &b'\n') 158 | .filter(|segment| !segment.is_empty()) 159 | .map(serde_json::from_slice) // FIXME: serde_json this bad boy 160 | .collect::, _>>() 161 | .expect("Error converting test buffer to JSON") 162 | .into_iter() 163 | .peekable(); 164 | 165 | // test messages at every depth for correctness 166 | while let Some(message) = messages.next() { 167 | // span IDs should NOT be propagated, but generated for each span 168 | assert_ne!( 169 | message.span_id, span_id, 170 | "Span IDs are the same, but should not be" 171 | ); 172 | 173 | if let Some(next_message) = messages.peek() { 174 | // span IDs should be different between spans 175 | assert_ne!( 176 | message.span_id, next_message.span_id, 177 | "Span IDs between messages are the same, but should not be" 178 | ); 179 | } 180 | 181 | // trace ID should be propagated to all messages and formatted in Cloud Trace format 182 | assert_eq!( 183 | message.trace_id, 184 | format!("projects/{PROJECT_ID}/traces/{trace_id}"), 185 | "Trace IDs are not compatible", 186 | ); 187 | 188 | // trace sampling should be disabled by default 189 | assert!(!message.trace_sampled) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/source_location.rs: -------------------------------------------------------------------------------- 1 | use helpers::{run_with_tracing, run_with_tracing_layer}; 2 | use mocks::MockDefaultEvent; 3 | 4 | mod helpers; 5 | mod mocks; 6 | 7 | #[test] 8 | fn includes_source_location() { 9 | let events = run_with_tracing::(|| tracing::info!("hello!")) 10 | .expect("Error converting test buffer to JSON"); 11 | 12 | let event = events.first().expect("No event heard"); 13 | assert!(event.source_location.file.ends_with("source_location.rs")); 14 | assert!(!event.source_location.line.is_empty()); 15 | assert!(event.source_location.line != "0"); 16 | } 17 | 18 | #[test] 19 | fn excludes_source_location() { 20 | let layer = tracing_stackdriver::layer().with_source_location(false); 21 | 22 | run_with_tracing_layer::(layer, || tracing::info!("hello!")) 23 | .expect_err("Failed to exclude source location fields from events"); 24 | } 25 | -------------------------------------------------------------------------------- /tests/valuable.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::disallowed_names)] 2 | #![cfg(all(tracing_unstable, feature = "valuable"))] 3 | use helpers::run_with_tracing; 4 | use mocks::{MockDefaultEvent, MockHttpEvent}; 5 | use serde::Deserialize; 6 | use std::fmt::Debug; 7 | use tracing_stackdriver::LogSeverity; 8 | use valuable::Valuable; 9 | 10 | mod helpers; 11 | mod mocks; 12 | 13 | #[test] 14 | fn handles_valuable_severity_override() { 15 | let events = run_with_tracing::(|| { 16 | tracing::info!( 17 | severity = LogSeverity::Notice.as_value(), 18 | "notice me, senpai!" 19 | ) 20 | }) 21 | .expect("Error converting test buffer to JSON"); 22 | 23 | let event = events.first().expect("No event heard"); 24 | assert_eq!(event.severity, "NOTICE"); 25 | } 26 | 27 | #[test] 28 | fn validates_structured_http_requests() { 29 | let request_method = http::Method::GET; 30 | let latency = std::time::Duration::from_millis(1234); 31 | let status = http::StatusCode::OK; 32 | let remote_ip = std::net::IpAddr::from([127, 0, 0, 1]); 33 | 34 | let http_request = tracing_stackdriver::HttpRequest { 35 | request_method: Some(request_method.clone()), 36 | latency: Some(latency), 37 | status: Some(status), 38 | remote_ip: Some(remote_ip), 39 | ..Default::default() 40 | }; 41 | 42 | let events = run_with_tracing::(|| { 43 | tracing::info!( 44 | http_request = http_request.as_value(), 45 | "http_request testing" 46 | ) 47 | }) 48 | .expect("Error converting test buffer to JSON"); 49 | 50 | let event = events.first().expect("No event heard"); 51 | assert_eq!( 52 | event.http_request.request_method, 53 | request_method.to_string() 54 | ); 55 | assert_eq!( 56 | event.http_request.latency, 57 | format!("{}s", latency.as_secs_f32()) 58 | ); 59 | assert_eq!(event.http_request.status, status.as_u16()); 60 | assert_eq!(event.http_request.remote_ip, remote_ip.to_string()); 61 | } 62 | 63 | #[derive(Debug, Deserialize, Valuable, PartialEq)] 64 | struct StructuredLog { 65 | foo: String, 66 | bar: std::collections::BTreeMap, 67 | } 68 | 69 | #[derive(Debug, Deserialize)] 70 | #[serde(rename_all = "camelCase")] 71 | struct MockStructuredEvent { 72 | structured_log: StructuredLog, 73 | } 74 | 75 | #[test] 76 | fn includes_valuable_structures() { 77 | let foo = "testing".to_string(); 78 | let mut bar = std::collections::BTreeMap::new(); 79 | bar.insert("baz".into(), 123); 80 | let structured_log = StructuredLog { foo, bar }; 81 | 82 | let events = run_with_tracing::(|| { 83 | tracing::info!( 84 | structured_log = structured_log.as_value(), 85 | "another message" 86 | ) 87 | }) 88 | .expect("Error converting test buffer to JSON"); 89 | 90 | let event = events.first().expect("No event heard"); 91 | assert_eq!(event.structured_log, structured_log); 92 | } 93 | --------------------------------------------------------------------------------